テレビ放送から取得したテキストを利用してみる

以前に書いた記事 [テレビ放送から諸々扱えそうなテキストを取得する](https://qiita.com/tsurime/items/4495de929755db0bd6b4) からの続きのようなものになります。

# はじめに
前回、テレビ放送から諸々に使えそうなテキストを出力しました。今回は、そのテキストを使って諸々やってみた、という話になります。WordCloud、Word2Vec、マルコフ連鎖による文章生成、あたりをやってみました。

前回は PX-S1UD で録画をしましたが、その後 PX-Q1UD という4チャンネルチューナーを購入しました。これにより、PXS1UD + PX-Q1UD の組み合わせで5番組を録画できるようになりましたので、せっかくですし各局を比べてもみようと思います。
特に断りがない限り、2月11日から28日までのNHK総合・日本テレビ・テレビ朝日・TBS・フジテレビの字幕になります（もう3月終わりですが、記事書き始めてから上げるまでに時間が経ってしまった）。

いわゆる「やってみた」系のネタ記事になりますが、各局の放送字幕を集めてテキスト処理……という話は見た記憶がないので、なんとなくどんな感じになるのか、面白さというものを感じていただければ、と。

# 前回からの補足
いろいろやる前に、前回の補足・修正などから。

## 整形ルールの追加・修正
assからテキストへの変換の際の整形ルールを追加しました。

1. 「＞（不等号・大なり記号）」が「＜（不等号・小なり記号）」が出てくることなく疲れた場合、「（発言者名）＞（発言内容）」とみなし、発言者名は除去。山括弧の場合も同様
1. 「:（コロン）」の左側は発言者名とみなして除去
1. 改行の前または後が平仮名の場合は、改行前・後で別単語とみなして空白を入れる
1. 改行の前または後がカタカナの場合は、前項に関わらず空白は入れない

スペース入れるようにしておけば、利用するとき形態素解析とかがきっとよきにはからってくれます（ぇー。

### ルール追加後のスクリプト
<details><summary>ルール追加後の整形用Pythonスクリプトを示します。</summary><div>

```ass2text.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import re
import jaconv

hirakana = "ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖゝゞゟ"
katakana = "゠ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ーヽヾヿ"

noserif = ['♬～', '♬〜', '♬~', '♪～', '♪〜', '♪~']
cmark = ['→', '➡']
eos = ['｡', '。', '!', '！', '?', '？', '⁉', '‼']

marubraket_open = ['(', '（']
marubraket_close = [')', '）']
kagibraket_open = ['｢', '「', '『']
kagibraket_close = ['｣', '」', '』']
yamabraket_open = ['＜', '<', '〈', '《', '≪']
yamabraket_close = ['＞', '>', '〉', '》', '≫']
kakubraket = [ '[', ']', '［', '］']

colon = [':', '：']

rebraket = re.sub(r'([\(\)\[\]])', r'\\\1', '|'.join(marubraket_open + marubraket_close + \
                                                kagibraket_open + kagibraket_close + \
                                                yamabraket_open + yamabraket_close + \
                                                kakubraket))
recolon = '|'.join(colon)

def HMSms2as(t):
    ht = re.split('[:.]', t)
    r = ( int(ht[0]) * 60 * 60 * 100 ) + \
        ( int(ht[1]) * 60 * 100) + \
        ( int(ht[2]) * 100 ) + \
        ( int((ht[3]+"0")[:2]) % 100 )
    return r

def ass2array(fn):
    b = False
    r = []

    try:
        f = open(fn)
        l = f.readline()
        while l:
            s = l.replace("\n","")    
            if 0 < len(s):
                if '[' == s[0]:
                    if '[events]' == s[0:8].lower():
                        b = True
                        l = f.readline()
                        continue
                    else:
                        b = False
                else:
                    if b:
                        if 'dialogue:' == s[0:9].lower():
                            c = 'ffffff'
                            d = s[10:].split(',')
                            if 'default' == d[3].lower():
                                txt = [x for x in re.split('({|})', ','.join(d[9:]).strip()) if not ''==x]
                                st = HMSms2as(d[1])
                                et = HMSms2as(d[2])
                                tb = False
                                buf = []
                                for t in txt:
                                    if "{" == t:
                                        tb = True
                                        continue
                                    if "}" == t and tb:
                                        tb = False
                                        continue
                                    if tb:
                                        m = re.search('\\\d{0,1}c&H([0-9a-fA-F]+)', t)
                                        if m:
                                            c = m.group(1)
                                    else:
                                        ta = [x for x in re.split('\\\\[nN]', t.replace('\\h', ' ')) if not ''==x]
                                        for tx in ta:
                                            ty = jaconv.z2h(jaconv.h2z(tx, digit=False, ascii=False), kana=False, digit=True, ascii=True)
                                            
                                            buf = [ty, c, st, et]
                                            r.append(buf)
            l = f.readline()
        f.close
    except Exception as e:
        print(e)
        pass
    return r

def talkcolon(text):
    r = []
    if 0 < sum([text.count(x) for x in colon]):
        ta = [x.strip() for x in re.split('(' + recolon + ')', text) if not ''==x]
        i=0
        for i in range(len(ta)):
            if ta[i] in colon:
                if 1 <= i:
                    tb = re.split('(\s)', r.pop(-1))
                    if 2 < len(tb) and ' ' in tb[-2]:
                        r.append(''.join(tb[0:-2]))
                else:
                    pass
            else:
                r.append(ta[i])
    else:
        r.append(text)

    return r

def flattern(items):
    r = []
    for t in items:
        if isinstance(t , list):
            r.extend(flattern(t))
        else:
            r.append(t)
    return r

def tsplit(text):
    r = []
    ta = flattern([talkcolon(x.strip()) for x in re.split('(' + rebraket + ')', text) if not ''==x])
    tb = {"maru": False, "kagi": False, "yama": False}
    ks = ""

    for t in ta:
        if t in marubraket_open:
            tb["maru"] = True
            continue
        if t in marubraket_close:
            tb["maru"] = False
            continue

        if t in kagibraket_open:
            tb["kagi"] = True
            rb = r.pop(-1) if 0 < len(r) and r[-1][-1:] not in kagibraket_close else ""
            ks = rb + t
            continue
        if t in kagibraket_close:
            tb["kagi"] = False
            t = ks + t
            ks = ""

        if t in marubraket_open:
            tb["maru"] = True
            continue
        if t in marubraket_close:
            tb["maru"] = False
            continue
        if t in yamabraket_open:
            tb["yama"] = True
            continue
        if t in yamabraket_close:
            if not tb["yama"] and 1 == len(r):
                r.pop(-1)
            tb["False"] = True
            continue

        if t in kakubraket:
            continue

        if tb["maru"]:
            pass
        elif tb["kagi"]:
            ks += t
        else:
            if not t in noserif:
                if t[:1] not in kagibraket_open and \
                   0 < len(r) and r[-1][-1:] in kagibraket_close:
                    r[-1] += t
                else:
                    r.append(t)
    if 0 < len(ks):
        r.append(ks)
    return r

def assarray2text(assary):
    j=0
    tary = []
    t = ""
    for i in range(len(assary)):
        if assary[i][0] in noserif:
            continue
        if 0 < i:
            if 500 > ( assary[i][2] - assary[i-1][3]):
                if assary[(i-1)][1] != assary[i][1]:
                    tary.extend(tsplit(t))
                    t = assary[i][0]
                else:
                    if t[-1:] in eos or t[-1:] in kagibraket_close:
                        tary.extend(tsplit(t))
                        t = assary[i][0]
                    elif t[-1:] in cmark:
                        t = t[:-1] + assary[i][0]
                    else:
                        sp = ""
                        if t[-1:] in hirakana or assary[i][0][0:] in hirakana:
                            sp = " "
                        if t[-1:] in katakana or assary[i][0][0:] in katakana:
                            sp = ""
                        t = t + sp + assary[i][0]
            else:
                tary.extend(tsplit(t))
                tary.append("")
                t = assary[i][0]
        else:
            t = assary[i][0]
    if "" != t:
        tary.extend(tsplit(t))

    return tary


if __name__ == '__main__':
    if 2 != len(sys.argv):
        print('Usage: # python %s filename' % sys.argv[0])
        quit()

    aary = ass2array(sys.argv[1])
    tary = assarray2text(aary)

for t in tary:
    print (t)
    pass
```
</div></details>

## 取得されるテキストの量
日や放送局によって差はありますが、一日一放送局の番組を録画して得られるテキストは 300～700kbytesほど、行数にすれば 5000～12000行ほどになります。
5000～12000行といっても、一行に複数の文が入ってる場合もありますのでおよその目安ですが、その程度と思ってください。

## 字幕が入ってる番組について
「日付が変わると字幕の入った番組がなくなる」とtwitterで呟いたところ、「[視聴覚障害者向け放送普及行政の指針](https://www8.cao.go.jp/shougai/suishin/seisaku_iinkai/k_37/pdf/s2-1.pdf) (PDF) によると『7時～24時の放送に字幕入れてね』とあるからそれに従っているのでは」と教えていただきました。
なるほど、例外はあるようですが、確かに日付変わるとパタリと字幕が止まりますし、7時前に始まる番組には字幕がほとんど入っていません。
字幕を目的なら24時間ではなく、7時～24時の17時間稼働でいいかもしれません。


# 本題
それではここから本題。放送字幕から取得したテキストをいろいろに使っていくことにします。

# 環境

OSは前回と同じく、Ubuntu18.04 です。
地上デジタルTVチューナーは先に書いたとおり、 PX-S1UD V2.0 に加え PX-Q1UD を利用して5チャンネル分の放送を録画・取得しています。
この5チャンネルは
・NHK総合
・日本テレビ
・TBS
・フジテレビ
・テレビ朝日
としています。

あと今回テキストを用いる上で、形態素解析エンジンにMeCabを、単語分かち書き辞書として mecab-ipadic-NEologd を使います。これらはインストール済みということで話を進めます。

# 取得した放送字幕テキストから Word Cloud を作成する

まずは Word Cloud を作ってみます

## WordCloud とは
WordCloudとはなにか、というと、単語の出現頻度を可視化したものです。
例えばNHKで放送された「みんなで筋肉体操」「みんなで筋肉体操（2）」の字幕から得られたテキストを WordCloud にするとこうなります

![kinniku.png](https://qiita-image-store.s3.amazonaws.com/0/171026/4a6e68f2-8171-2dde-d12b-017afffc44cf.png)

文字の間から筋トレしている様子がみてとれますね？
こういうことです（どういうこと？

まぁ筋トレはおいといて。文中でどのような単語が良く使われるのか、というのがわかります。テレビの字幕から取得できるテキストに対してWordCloudを作れば、メディアがなにを話題にしているのかが一目でわか……るのかな？

今回は毎日5チャンネル分のデータがありますので、日毎・放送局毎にまとめていき、放送局毎の扱う話題の違いというのを見てみたいと思います。

ところで。

```
$ echo '筋肉は裏切らない' | mecab -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd
筋肉は裏切らない        名詞,固有名詞,一般,*,*,*,筋肉は裏切らない,キンニクハウラギラナイ,キンニクワウラギラナイ
EOS
```

固有名詞、なの……？

## WordCloud スクリプト

WordCloudのPythonライブラリがありましたので、そちらを使います。`pip install wordcloud` などとしてインストールしておいてください（詳しいやり方は忘れました）。

使い方は単純で、文章をMaCabで単語ごとに分けたものをツッコむだけです。今回は名詞だけとしました。他の品詞を入れたらエラいことになったので、見やすいように、というのも^^;

<details><summary>WordCloud出力用 Pythonスクリプト</summary><div>

```wordcloud.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
import os
from wordcloud import WordCloud
import random
import MeCab

wordcloudimg_dir = "wcloud/"
colornum = 0
colors = {
    "21": "#2f26b8",
    "22": "#2e974b",
    "23": "#7803e2",
    "24": "#cdaa07",
    "25": "#980247",
    "26": "#57585e",
    "27": "#666666"
}
stopwords = [
    'ある', 'なる', 'する', 'きる', 'できる',
    'もん',
    'そこ', 'ここ', 'どこ', 'こと', 'これ', 'それ', 'あれ', 'こちら',
    'ため', 'なん', 'わけ',
    'さん', 'ちゃん', 'さま',
    'やる', 'いる', 'いう', 'いく',
    'ところ',
    'そう', 'よう', 'とき', 'もの', 'みたい', 'ほう',
    '思う', '言う', '見る', '感じ',
    'あと', 'ホント', 'やつ', 'たち',
    '自分', 'あなた', '皆さん', 'うち',
    '今日', '明日', '昨日', 'きょう', 'あす', 'きのう',
    '今回'
]

wordclass = {
    '名詞': True,
    '動詞': False,
    '形容詞': False,
    '副詞': False,
}
wordclass2 = {
    '動詞': ['自立']
}
wcary = [x for x in wordclass if wordclass[x]]

def mecab_analysis(t):
    mt = MeCab.Tagger("-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd")
    mt.parse('')
    node = mt.parse(t) 
    res = []
    words = node.split("\n")

    for w in words:
        s = w.split("\t")
        if 2 <= len(s):
            f = s[1].split(",")
            b = False
            if f[0] in wcary:
                b = True
                if f[0] in wordclass2 and f[1] not in wordclass2[f[0]]:
                    b = False
            if b:
                if "*" == f[6]:
                    res.append(s[0]) # 出現単語の表層形を入れる場合はこっち
                else:
                    res.append(f[6]) # 出現単語の原型を入れる場合はこっち
    return res

def color_func(word, font_size, position, orientation, random_state, font_path):
    c = "#000000"
    if colornum in colors:
        c = colors[colornum]
    else:
        c = random.choice(list(colors.values()))
    return c

if __name__ == '__main__':
    if 2 != len(sys.argv):
        print('Usage: # python %s text_filename' % sys.argv[0])
        quit()

    filename = sys.argv[1]
    baseext = os.path.splitext(os.path.basename(filename))
    c = baseext[0].split('_')
    colornum = c[-1]

    words = []
    try:
        f = open(filename)
        l = f.readline()
        while l:
            s = l.replace("\n","")    
            words.extend(mecab_analysis(s))
            l = f.readline()
        f.close

    except Exception as e:
        print(e)
        pass

    wctext = " ".join(words)
    #print(wctext)

    wordcloud = WordCloud(background_color="white",
                          color_func=color_func,
                          max_words=300,
                          collocations = False,
                          font_path="/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
                          width=960,height=600,
                          stopwords=set(stopwords)).generate(wctext)

    wordcloud.to_file(wordcloudimg_dir + baseext[0] + '.png')
```
</div></details>

## WordCloud 結果

2月11日から5チャンネル分の字幕取得をしています。2月11日～28日の字幕から生成したWordCloudからいくつかピックアップして見てみます。
字の色が灰色はNHK、緑・TBS、赤・日本テレビ、黄色・テレビ朝日、青・フジテレビです。

![20190211.png](https://qiita-image-store.s3.amazonaws.com/0/171026/2baf7689-e592-5860-c10a-846d5b8fe731.png)
TBSはなんの番組をやっていたかわかりやすいですね。「デモ」がテレビ朝日でのみあるので、なんだろうと調べたら「建国記念日ということでデモ」の特集を組んだ番組があった、ということのようで。

![20190212.png](https://qiita-image-store.s3.amazonaws.com/0/171026/90f1f6d7-9eaa-f068-f351-460b149c3d4e.png)
![20190213.png](https://qiita-image-store.s3.amazonaws.com/0/171026/cd205ff1-01cc-8917-5231-d7db20d7f200.png)
競泳の池江選手が白血病であることを公表しました。NHKは他局に比べて本人の名前と病名の扱いが少ないようです。この辺、ワイドショーで大々的に扱った民放との違いなのでしょうか？

![20190221.png](https://qiita-image-store.s3.amazonaws.com/0/171026/c5df55f0-01f3-c2d7-188e-d71fdeb83a32.png)
2月21日 21時22分頃、北海道で震度6弱の地震がありました。が、フジテレビにはなぜか「地震」の文字が見あたりません。調べてみたら、フジテレビで地震の報道に割かれた時間は5分程度だったようです。さすがに他局に比べて極端に少なかったようです。

![20190222.png](https://qiita-image-store.s3.amazonaws.com/0/171026/7db75646-c6fe-245c-44ac-922e03290ff2.png)
NHKとフジテレビ以外ははやぶさ2の扱いがとても少ないようで。宇宙開発に興味ないのかしら？^^;

![20190228.png](https://qiita-image-store.s3.amazonaws.com/0/171026/14044f12-961a-bb48-9c25-87dce54ec96d.png)
さすがに全局大きくなる文字が揃いました。

## WordCloudの結果・課題

日毎・局毎のWordCloudを見てみましたが、だいたいどの局がその日なにを中心に放送していた、というのはわかったと思います。
局毎の違い、というのはそこまで出ていない感じがしますが、ここに載せてない分も含めてみてるとNHKは話題を翌日に持ち越さない、というのがある気がします。民放はワイドショーなどで扱い続ける、ということなのでしょうか。

日にもよるのですが、およそ50回で一目で目立つ大きさになります。なので、ドラマ、例えば刑事ドラマなどで容疑者の名前が出てくると連呼されることになりますが、その場合は「なんか聞いたことない人名がやたら大きい」となります。
その日の話題ということを中心に抽出したいのであれば、ドラマやアニメなどを除外するなどした方がよいでしょう。


# 取得した放送字幕テキストから Word2Vec のmodelを作成する

## Word2Vecとは
Word2Vecとは、単語の特徴をベクトルで表現するものです。これにより、単語動詞の足し算引き算などができるようになります。
よく使われるものでは、
「王様」 - 「男性」 + 「女性」 = 「女王」
なんていう計算ができるようになる、と言われます。
例によって試しに筋肉体操でmodelを作って、「筋トレ」+「腕立て伏せ」-「腹筋」を入れてみたところ

```txt
positive : ['筋トレ', '腕立て伏せ'], negative : ['腹筋']
声 0.997889518737793
筋肉は裏切らない 0.9977787137031555
」 0.9977445006370544
バーベル 0.9977430105209351
出し手 0.997633695602417
みんなで筋肉体操 0.9976252317428589
配分 0.9975670576095581
栄養 0.9975487589836121
問題 0.9975254535675049
アップ 0.9975100755691528
```
うん、よくわからない結果になりました^^;
サンプルが少ない、内容にある意味偏りがありすぎる、というのもあるかと。
偏らないよう、2月11日から28日の18日分を入れて結果をみていきましょう。

## Word2Vec スクリプト

gensim という便利なものがあるので、こちらを `pip install gensim` などとしてインストールします。
分かち書きしたテキストを 1. のスクリプトで用意し、それを 2. のスクリプトに入れることでWord2Vecのmodelを作成します。Word2Vecのパラメタはもう少し考えた方がいい気もしますが、とりあえずコレでいきます。

<details><summary>1. テキスト→分かち書き スクリプト</summary><div>

```txt2wakachi.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
import os
import MeCab

class Wakachi():
    def __init__(self, filename, *, wordclass=None, wordclass2=None):
        self.filename = filename
        self.wc = wordclass
        self.wc2 = wordclass2
        self.f = open(filename)
        self.res = []

    def sentence_to_word(self, s):
        return self._mecab_analysis(s)

    def wakachi(self, *, genkei=True):
        try:
            l = self.f.readline()
            while l:
                words = []
                s = l.replace("\n","")    
                words.extend(self._mecab_analysis(s, genkei))
                o = " ".join(words)
                self.res.append(o)
                l = self.f.readline()
        except Exception as e:
            print(e)

        self.f.close

    def output(self):
        for r in self.res:
            print(r)
       
    def _mecab_analysis(self, t, g):
        mt = MeCab.Tagger("-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd")
        mt.parse('')
        node = mt.parse(t) 
        res = []
        words = node.split("\n")

        for w in words:
            s = w.split("\t")
            if 2 <= len(s):
                f = s[1].split(",")
                b = True if self.wc is None else False 
                if self.wc is not None and f[0] in self.wc:
                    b = True
                    if self.wc2 is not None and f[0] in self.wc2 and f[1] not in self.wc2[f[0]]:
                        b = False
                if b:
                    if g:
                        if "名詞" == f[0] or "*" == f[6]:
                            res.append(s[0]) # 出現単語の表層形
                        else:
                            res.append(f[6]) # 出現単語の原型
                    else:
                        res.append(s[0])
        return res

if __name__ == '__main__':
    if 2 != len(sys.argv):
        print('Usage: # python %s text_filename' % sys.argv[0])
        quit()
    filename = sys.argv[1]
    w = Wakachi.Wakachi(filename)
    w.wakachi()
    w.output()
```
</div>


<details><summary>Word2Vec model出力スクリプト</summary><div>

```w2vec.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import os
from gensim.models import word2vec

import argparse

if __name__ == '__main__':
    try:
        parser = argparse.ArgumentParser(
            prog = sys.argv[0],
            usage = "python3 %s -t text_filename -o output_model" %sys.argv[0]
        )
        parser.add_argument('-t', '--text', help="input text file", required=True)
        parser.add_argument('-o', '--output', help="output model file", required=True)
        parser.add_argument('-i', '--input', help="input model file")

        args = parser.parse_args()
    except:
        exit()

    print(args)

    input_textname = args.text
    output_modelname = args.output
    input_modelname = args.input

    print("wakachi text: %s" % input_textname)
    sentences = word2vec.Text8Corpus(input_textname)

    if input_modelname is None:
        model = word2vec.Word2Vec(sentences, size=100, sg=1, min_count=1)
    else:
        print("input model: %s" % input_modelname)
        model = word2vec.Word2Vec.load(input_modelname)
        model.build_vocab(sentences, update=True)
        model.train(sentences, total_examples=model.corpus_count, epochs=model.iter)

    print("output model: %s" % output_modelname)
    model.save(output_modelname)
```
</div>

ここまでの 1. と 2.で作った model から指定した語に近いベクトルの単語を出力するスクリプトを以下 3. に示します。
使い方は

```
% python3 w2v_check.py jimaku.model 単語1 単語2 -単語3 -単語4
```

などとすると、先に書いたような単語のベクトルに対する演算で「単語1 + 単語2 - 単語3 - 単語4」に近いベクトルの単語を10個出力します。

<details><summary>3. Word2Vec 出力スクリプト</summary><div>

```w2v_check.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import os
from gensim.models import word2vec

N = 10

if __name__ == '__main__':
    if 3 > len(sys.argv):
        print('Usage: # python %s model word1 word2...' % sys.argv[0])
        quit()

    filename = sys.argv[1]
    pwords = []
    nwords = []
    for i in range(2, len(sys.argv)):
        if '-' == sys.argv[i][0]:
            nwords.append(sys.argv[i][1:])
        else:
            pwords.append(sys.argv[i])

    simwords = []
    model = word2vec.Word2Vec.load(filename)
    try:
        res = model.wv.most_similar(positive=pwords, negative=nwords, topn=N)
        for r in res:
            simwords.append({'word':r[0], 'distance':r[1]})
    except:
        pass

    print("positive : ", end="" )
    print(pwords, end=", ")
    print("negative : ", end="" )
    print(nwords)

    for s in simwords:
        print(s['word'], s['distance'])


```
</div>


## 各局比較
それでは、各局比較して見てみます。
positiveは足し算・negativeは引き算した単語です。
「全体」はすべての局の字幕を cat してつなげたもの、WikipediaはWikipediaからのデータを、それぞれmodelにしたものにかけています。
スコアは考えないで、上位10件づつを出力しています。

### positive : ['政府'] / negative : []
||NHK総合|日本テレビ|テレビ朝日|TBS|フジテレビ|全体|Wikipedia|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|1|厚生労働大臣|韓国側|朝鮮戦争|韓国側|辺野古|投票結果|中央政府|
|2|公明党|慰安婦問題|ホワイトハウス|日本側|普天間基地|与党|英政府|
|3|防衛省|原告|朝鮮半島|示唆|ノーベル平和賞|沖縄県民|マカオ政庁|
|4|沖縄県|議長|議会|懲戒|埋め立て|住民投票|自治政府|
|5|普天間基地|日韓|トランプ氏|厚生労働省|韓国側|普天間|当局|
|6|返還|謝罪|民主党|辺野古|移設|移設|特別自治区|
|7|アベノミクス|ムン|国連|官邸|日本政府|玉城|米財務省|
|8|与党|北朝鮮の非核化|韓国側|処分|安倍総理|民意|奉ソ協定|
|9|当面|意向|日本政府|民意|県民|辺野古|ドーズ委員会|
|10|ムン|辺野古|終結|総理官邸|国会議長|沖縄県|マラヤ連合|

局ごとにベクトルの近い語、として出てくるものが「日本の内側」と「海外」とで差が微妙に出ている気がします。面白いのは、全体をつなげてみると沖縄について出てくる、という点ですね。「住民投票」なんかは各局毎では出てこないのが全体になると出てくる、という（Word2Vecの仕組み上当然ですが）。

### positive : ['アメリカ'] / negative : []
||NHK総合|日本テレビ|テレビ朝日|TBS|フジテレビ|全体|Wikipedia|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|1|北朝鮮|中国|北朝鮮|北朝鮮|トランプ大統領|北朝鮮|米国|
|2|中国|北朝鮮|韓国|トランプ大統領|中国|中国|カナダ|
|3|韓国|協議|中国|特別|金正恩|トランプ政権|イギリス|
|4|ロシア|ミサイル|朝鮮半島|韓国|北朝鮮|トランプ大統領|英国|
|5|両国|核|非核化|ポンペオ|ベトナム|パレスチナ|オーストラリア|
|6|北朝鮮側|トランプ大統領|トランプ大統領|中国|ロシア|習近平|アメリカ合衆国|
|7|朝鮮半島|北朝鮮の非核化|協議|ビーガン|桜田|貿易交渉|英米|
|8|日韓|交渉|国内|国務長官|交渉|北ベトナム|合衆国|
|9|トランプ政権|貿易|米朝首脳会談|ミサイル|協議|副大統領|ニュージーランド|
|10|首相|ビーガン|南北|大使館|安倍総理|指導部|欧米|

これは時期的にこうなるでしょう、としか^^;

### positive : ['地震'] / negative : []
||NHK総合|日本テレビ|テレビ朝日|TBS|フジテレビ|全体|Wikipedia|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|1|震度6弱|揺れ|震度6弱|震度6弱|茨城県|揺れ|大地震|
|2|揺れ|厚真町|火災|厚真町|厚真町|震度6弱|津波|
|3|去年9月|震度6弱|震源|停電|券売機|厚真町|余震|
|4|発生|胆振|不審火|揺れ|横浜市|津波|本震|
|5|厚真町|北海道胆振東部地震|土砂崩れ|震源|震度6弱|北海道胆振東部地震|巨大地震|
|6|震度7|震源|胆振|大阪府|20日|震源|マグニチュード7.3|
|7|観測|震度7|厚真町|地方|火災|震度7|震源|
|8|震源|気象庁|クラス|観測|自動券売機|胆振|直下型地震|
|9|震度3|雪崩|停電|ホンダ|沿岸部|余震|マグニチュード7.6|
|10|北海道厚真町|津波|津波|濃霧|JR東日本|気象庁|地震動|

大体、2/21の北海道での地震についてのことになっています。が、WordCloudで見たのと同じく、フジテレビはなんでしょう？

### positive : ['ダウンタウン'] / negative : ['松本']
||NHK総合|日本テレビ|テレビ朝日|TBS|フジテレビ|全体|Wikipedia|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|1| - |しんじょう君|円|増進|ルル|富士|歴史地区|
|2| - |☆|1本|,|ティティー|じゅん散歩|フィナンシャル・ディストリクト|
|3| - |レフ|1000円|旨み|ヘイヘイホー|滝|マーケット・ストリート|
|4| - |厚生省|相場|瓶詰|ティー|公園|ミッドタウン|
|5| - |山田真|億|up|騒々しい|紅葉|メイン・ストリート|
|6| - |生ガキ|千|トロトロ|a|多摩川|ウォルト・ディズニー・ワールド・リゾート|
|7| - |Climb|畳|辛み|ギャー|隅田川|リサーチ・トライアングル|
|8| - |24時間営業|グラム|グラタン|’|ブルー|カイルア・コナ|
|9| - |ライ|類|パサ|keep|罪と罰|サンガブリエル・バレー|
|10| - |GUESS|7500円|生クリーム|s|JAPAN|チャペルヒル|

芸人コンビの「ダウンタウン」を入れたつもりです……が、まぁ、Wikipediaは納得です^^;
「浜田」のようなものが出てくると期待したのですが。さすがにまだデータが少ないんでしょうか。ダウンタウンのレギュラーが多い日本テレビあたりではソレっぽい結果になると思ったのですが。

### positive : ['映画'] / negative : []

||NHK総合|日本テレビ|テレビ朝日|TBS|フジテレビ|全体|Wikipedia|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|1|ドラマ|ドラマ|ドラマ|ドラマ|ドラマ|ドラマ|映画作品|
|2|主演|出演|アーティスト|曲|作品|主題歌|ハリウッド映画|
|3|オーディション|九月の恋と出会うまで|名曲|字|共演|名作|ミュージカル映画|
|4|アニメ|主演|アニメ|主人公|舞台|作品|テレビ映画|
|5|CM|番組|歌|番組|企画|原作|短編映画|
|6|ヒーロー|女優|ライブ|出演|番組|ミュージカル|青春映画|
|7|ドキュメンタリー|アニメ|漫画|CM|歌|主演|ゾンビ映画|
|8|作品|ミュージックビデオ|シリーズ|業務スーパー|アーティスト|朝ドラ|劇映画|
|9|ミュージカル|作品|主題歌|天|伝説|漫画|sf映画|
|10|ショー|数々|主人公|コバヒロ|曲|アニメ|長編映画|

各局で推し（= 制作にからんでいる）映画タイトルが出るかな？と思いましたが、そうでもないようです。というか、局の差が少なくかなり似通った結果になっているように思えます。

### positive : ['プリキュア'] / negative : []
||NHK総合|日本テレビ|テレビ朝日|TBS|フジテレビ|全体|Wikipedia|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|1| - | - |ライダー| - | - |トゥインクル|キュアフェリーチェ|
|2| - | - |Ⅱ| - | - |えがく|モフルン|
|3| - | - |ジオウ| - | - |ジオウ|セーラー戦士|
|4| - | - |?「| - | - |Ⅱ|キュアミラクル|
|5| - | - |仮面ライダー| - | - |飛行機雲|キョウリュウジャー|
|6| - | - |ウォズ| - | - |ウォズ|キュアモフルン|
|7| - | - |わたし| - | - |あこがれ|レフィ|
|8| - | - |ざんざん| - | - |ジオウ・ジオウ・ジオウ|キュアトゥインクル|
|9| - | - |キカイ| - | - |ライダーキカイ|ドキドキ!プリキュア|
|10| - | - |ライダーキカイ| - | - |トゥインクルプリキュア|ここたま|

テレビ朝日以外では「プリキュア」にまつわる単語はないでしょうから、結果が出てこないであろうと考えていましたので、期待通りです。ライダー・ジオウ・ウォズだのが出てくるのはニチアサですね^^;。ある意味納得です。
こちらもテレビ朝日単体での出力と全体とでは結果が異なりますね。

## Word2Vec の結果・課題

字幕取得した期間が2週間強、という短期間のためもあるでしょうが、当然ながらその期間に現れなかった単語についてはベクトルが求められず。
逆に考えれば「ある期間に対して話題になったもの」同士の関連性を取るには都合が良い気はします。
長期間にわたり取り続けるとどういった結果になるのか、どう変化するのか、というのは興味ありますのでしばらく続けてみたいと思います。


# 取得した放送字幕テキストからマルコフ連鎖による文章生成を行う

マルコフ連鎖とは……[Wikipedia の該当ページ](https://ja.wikipedia.org/wiki/%E3%83%9E%E3%83%AB%E3%82%B3%E3%83%95%E9%80%A3%E9%8E%96)を参照してください。
大したことはやってません。
一応、例によって筋肉体操のテキストでの生成をやってみたのですが、違和感が仕事しない文章しか出てきませんでした。当然ですが、テンプレートが決まっているような番組だけで生成した場合はテンプレート通りの文章しか生成できませんね。

スクリプトは以下です

<details><summary>マルコフ連鎖生成用スクリプト</summary><div>

```txt2chain.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
import os
import re
import argparse
import pickle

import MeCab

braket = ['「', '」', '『', '』']
eos = ['｡', '。', '!', '！', '?', '？', '⁉', '‼']
reeos = re.sub(r'([\!\?])', r'\\\1', '|'.join(eos))


def filetosentence(f):
    res = []
    try:
        f = open(filename)
        l = f.readline()
        while l:
            s = l.replace("\n","")
            if 0 < len(s):
                sa = [x.strip() for x in re.split('(' + reeos + ')', s) if not ''==x]
                t = ""
                while sa:
                    sb = sa.pop(0)
                    if sb in eos:
                        res.append(t + sb)
                        t = ""
                    else:
                        t = sb
                if 0 < len(t):
                    res.append(t)
            l = f.readline()
        f.close
    except Exception as e:
        print(e)
        pass

    return res

def mecab_analysis(t):
    mt = MeCab.Tagger("-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd")
    mt.parse('')
    node = mt.parse(t) 
    res = []
    words = node.split("\n")
    for w in words:
        s = w.split("\t")
        if s[0] in braket:
            continue
        if 2 <= len(s):
            f = s[1].split(",")
            p = { "surface":s[0],
                  "base": f[6] if "名詞" == f[0] or "*" == f[6] else s[0],
                  "pos":f[0],
                  "pos1": f[1]
                }
            res.append(p)
    return res

def make_chain(sentences, *, chain={}):
    for s in sentences:
        morphemes = mecab_analysis(s)
        if 0 < len(morphemes):
            i=0
            w = [""] * 2
            start = True
            while morphemes:
                m = morphemes.pop(0)
                w.pop(0)
                w.append(m["surface"])
                k = tuple(w)

                if all(["" != x for x in w]):
                    if k not in chain:
                        chain[k] = {
                            "start": start,
                            "morphemes": m["surface"],
                            "next":[]
                        }
                    else:
                        chain[k]["start"] = chain[k]["start"] or start

                    if 0 < len(morphemes):
                        chain[k]["next"].append(morphemes[0])
                    else:
                        chain[k]["next"].append(None)
                    start = False
    return chain

if __name__ == '__main__':

    try:
        parser = argparse.ArgumentParser(
            prog = sys.argv[0],
            usage = "python3 %s -t text_filename -o output_chain" %sys.argv[0]
        )
        parser.add_argument('-t', '--text', help="input text file", required=True)
        parser.add_argument('-o', '--output', help="output chain file", required=True)
        parser.add_argument('-i', '--input', help="input chain file")

        args = parser.parse_args()
    except:
        quit()

    filename = args.text
    output_filename = args.output
    input_filename = args.input

    if input_filename is None:
        chain = {}
    else:
        with open(input_filename, 'rb') as f:
            chain = pickle.load(f)

    sentences = filetosentence(filename)
    chain = make_chain(sentences, chain=chain)

    with open(output_filename, 'wb') as f:
        pickle.dump(chain, f)
```
</div>

<details><summary>文章生成用スクリプト</summary><div>

```gentext.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import os
import pickle
import random

def gen_sentence(chain):
    sentence = ""

    chainstart = [x for x in chain if chain[x]["start"]]
    k = random.choice(chainstart)
    while chain[k]['next'] is not None:
        w = list(k)
        w.pop(0)
        sentence += k[0]
        if 'next' in chain[k]:
            n = random.choice(chain[k]['next'])
            if n is not None:
                w.append(n['surface'])
            else:
                sentence += k[1]
                break
        k = tuple(w)

    return sentence

if __name__ == '__main__':
    if 2 > len(sys.argv):
        print('Usage: # python %s chain_filename count' % sys.argv[0])
        quit()

    filename = sys.argv[1]
    count = int(sys.argv[2]) if 2 < len(sys.argv) else 10

    with open(filename, 'rb') as f:
        chain = pickle.load(f)

    for i in range(count):
        s = gen_sentence(chain)
        print(s)
```
</div>

## 文章作成
作った文章からいくつか抽出してみました。各局の特色が出て……るかしら？

### NHK総合
>室内には、豚トロ煮。
2018年に報道するのは長崎市、長崎県雲仙市、長崎県南島原市、そして日本政府は拉致問題の難しさになりまんぷくラーメン。
怒られそうなのは職員が受付などを検討して資格を取ってみます。
やわらかめのファンデーションをつけて投票しますと一緒に暮らせるか一緒に備えてシャワー室にはやや角ばっていてやさしい酸味がマイルドになってるんですけど見えへんは見えてくるんですけど。
ただ区によりますと朝起きて人類全体の4割が過労死ラインを超える岩石が残されております。

### 日本テレビ
>きっとお話することができない看板見つけちゃいました。
グレムリンに変わるということを期待し、そうなんですけれども、別問題ですぅ!
作り方を拝見したい自治体に確認して家庭築き上げたいと思います。
6年前に競技化させ、見事、着陸。
夜もうな重だけをぐるりと回すようなことができましたが、花粉も防いでスキンケアまでできる

### テレビ朝日
>当時過労死だということで行ってたらちゃんと正式にあいさつをしていて更には3対3で戦います。
今日2次試験が近付くにつれ以前その本心を話したら…めちゃくちゃヨネスケさんだ。
道の駅では、マッチング実行をクリックするだけ。
経済が悪いという人がバーッと俺のチョコ食べてほしい物が転がったりともみなかったということを言ってて。
亡き母が3人は実際に来ています。

### TBS
>トイレ行きはったら頼まなくてオフィスにも影響が広がる現地を取材。
フレーバーティーを使用した禁断の過去をよく読むので、もう言わなくちゃいけません
人類で初めて出場した疑いで昨日2人は早速ですが、最多得票となった漱石ですが、トラ ンプ大統領って例えば移民のベビーシッターを雇った過去は変えたくないって
しつけというある意味抽象的なこともありますけどなんとか続けているので、こういうものをつくらずに禁止する条例案を都議会に児童虐待防止条例案を都議会に提出した鶏を専用のオーブンは…
その15分後に口止め料を不倫相手でした

### フジ
>それバレたらママとシスターに確認しました。
踊り疲れた老け顔がリセットされる広告収入があるとみた
ハッピーなところいっぱいあるのでそんなに私はアナウンサーに伝えたいという状況なんだけど。
ああ、ちょっと重い展開になった吉田さんで終わるんだね。
秋葉原やオタク文化が注目される横浜では給水活動が行われたの?

## 文章生成の結果・課題
ランダムを多用する文章生成なんだから局による差とかあったもんじゃない^^;。
文章を生成する際のサンプルとしては、プロが作るこなれた文章が多くでてくるので使い勝手はよさそうに思えます。
twitterなどSNSから文章を集めて同様に文章生成するよりも、普通の言葉が使われる感じはあるのではないでしょうか？とはいえ、ランダムなので生成された文章の発狂っぷりは同レベルな気はします。

# 最後に
2月11日から28日、という二週間強という短期間の字幕のみでやった結果にすぎませんので、どうなんだろう？という部分も多々ありますが。
テレビ放送字幕というのは、時期的な話題を観測するテキストとしての価値はあるといえるのではないでしょうか。もっとも使い方によってはWordCloudでみたように番組内容を絞る必要は出てくるでしょう。
Word2vecでみたように短期ゆえに出てこない語というのもあります。その場合、「出てこなかった」ということが価値にするような使い方ができるのであれば勝利（？）でしょう。出てこないで困る、という場合には別で語を取り込む必要はありそうです。
また、文章のサンプル、という点から見た場合は、少なくとも一度はプロの編集が入った後の言葉・文章になります。それでも誤字はありますがSNSなどで集めるよりははるかに少ないですし、なにより「にゃーん」や草が生えたりすることは滅多にありません^^;
前回も書いたのですが、「テキスト情報が常に得られるデバイス」としてみた場合、なかなかに使いでのある文章が集まると思いますので、やってみると面白いのではないかと思います。

#余談
私は関東住まいなので在京キー局ばかりになっていますが。地方局でやったらどういう情報が集まるのかなぁ、というのは気になるところです。