言語処理100本ノック2020 (50~54)

# はじめに
本記事は言語処理100本ノックの解説です。
100本のノックを全てこなした記録をQiitaに残します。
使用言語はPythonです。

https://nlp100.github.io/2020/ja/

今回は第6章: 機械学習(50~54)までの解答例をご紹介します。

>本章では，Fabio Gasparetti氏が公開しているNews Aggregator Data Setを用い，ニュース記事の見出しを「ビジネス」「科学技術」「エンターテイメント」「健康」のカテゴリに分類するタスク（カテゴリ分類）に取り組む．

# 50. データの入手・整形
>News Aggregator Data Setをダウンロードし、以下の要領で学習データ（train.txt），検証データ（valid.txt），評価データ（test.txt）を作成せよ．
>
>1. ダウンロードしたzipファイルを解凍し，readme.txtの説明を読む．
>2. 情報源（publisher）が”Reuters”, “Huffington Post”, “Businessweek”, “Contactmusic.com”, “Daily Mail”の事例（記事）のみを抽出する．
>3. 抽出された事例をランダムに並び替える．
>4. 抽出された事例の80%を学習データ，残りの10%ずつを検証データと評価データに分割し，それぞれtrain.txt，valid.txt，test.txtというファイル名で保存する．ファイルには，１行に１事例を書き出すこととし，カテゴリ名と記事見出しのタブ区切り形式とせよ（このファイルは後に問題70で再利用する）．
>
>学習データと評価データを作成したら，各カテゴリの事例数を確認せよ．

```python:コード
import pandas as pd
from sklearn.model_selection import train_test_split
df = pd.read_table("[PATH]/NewsAggregatorDataset/newsCorpora.csv", index_col=0,
                   names = ("ID", "TITLE", "URL", "PUBLISHER", "CATEGORY", "STORY", "HOSTNAME", "TIMESTAMP"))
df_1 = df[(df["PUBLISHER"]=="Reuters")|(df["PUBLISHER"]=="Huffington Post")|(df["PUBLISHER"]=="Businessweek")|
            (df["PUBLISHER"]=="Contactmusic.com")|(df["PUBLISHER"]=="Daily Mail")].loc[:, ["CATEGORY", "TITLE"]]#情報源をキーとした抽出
df_1 = df_1[~df_1["TITLE"].str.contains("http")]
df_Train, df_Test = train_test_split(df_1, test_size=0.2)
df_Test, df_Val = train_test_split(df_Test, test_size=0.5)

df_Train.to_csv("[PATH]/train.txt", sep='\t', index=False)
df_Test.to_csv("[PATH]/test.txt", sep='\t', index=False)
df_Val.to_csv("[PATH]/valid.txt", sep='\t', index=False)
```

```text:出力結果(train.txt)
CATEGORY	TITLE
e	Game of Thrones' Jack Gleeson On Joffrey, Screen Deaths And His Indefinite  ...
b	Euro-Area Economic Confidence Increases More Than Expected (1)
m	China's Kids Get Exposed to Cigarette Smoke at Middle School, National Survey  ...
b	Costco Employees Happier With Pay Than Many In Silicon Valley
e	Marvel Appoint Scott Derrickson To Direct 'Doctor Strange'
~~~~~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~~~~~~~
```
>学習データと評価データを作成したら，各カテゴリの事例数を確認せよ．
```text:コマンド
$ wc -l "[PATH]/train.txt"
$ wc -l "[PATH]/test.txt"
$ wc -l "[PATH]/valid.txt"
```
```text:出力結果
10672 #train
1335 #test
1335 #valid
```

**コメント**
6章ではpandasを使っていきます。pandasを上手く使えるとfor文で書くよりも速く処理ができます。

# 51. 特徴量抽出
>学習データ，検証データ，評価データから特徴量を抽出し，それぞれtrain.feature.txt，valid.feature.txt，test.feature.txtというファイル名で保存せよ． なお，カテゴリ分類に有用そうな特徴量は各自で自由に設計せよ．記事の見出しを単語列に変換したものが最低限のベースラインとなるであろう．


```python:コード
import collections
import re
import pandas as pd

def Process(lines):
    sign_regrex = re.compile('[!"#$%&\'()*+,-./:;<=>?@[\\]^_`|＄＃＠£â€™é\n]')
    lines = sign_regrex.sub("", lines)
    lines = re.sub("(\d+)", r" \1 ", lines)
    texts = lines.split(" ")
    texts = list(filter(lambda x:x, texts))#空リスト削除
    word_list = list(map(lambda x:x.lower(), texts))#小文字にする
    return word_list

def MakeDict(name):
    f = open("[PATH]/{}.txt".format(name), "r")
    lines = f.readlines()
    f.close()
    word_list = []
    for line in lines:
        word_list_temp = Process(line)
        word_list.extend(word_list_temp)
    c = collections.Counter(word_list).most_common()
    word_dic = {}
    i = 1
    for word in c:
        if int(word[1]) > 1:
            word_dic[word[0]] = i
            i += 1
    return word_dic

def MakeOneHot(text):
    temp = []
    word_list = Process(text)
    base_list = [0]*len(GlobalWordDict)
    for word in word_list:
        try:
            base_list[GlobalWordDict[word]] = 1
        except:
            pass
    return base_list

def MakeFeatureText(name):
    df = pd.read_table("[PATH]/{}.txt".format(name))
    df_2 = pd.DataFrame(list(df["TITLE"].apply(MakeOneHot)))
    df_3 = pd.concat([df, df_2], axis=1)
    df_3.to_csv("[PATH]/{}.feature.txt".format(name))

GlobalWordDict = MakeDict("train") #辞書をグローバル変数で定義
MakeFeatureText("train")
MakeFeatureText("test")
MakeFeatureText("valid")
```

**出力結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3579871/a3484163-7b6f-89f8-eb29-7d7ab1df90a7.png)
※TITLEは原文のまま

**コメント**
前処理が大事です。本問題では記号の削除、数字と文字を分ける、大文字を小文字するを実施。辞書作成はフェアにtrainデータで作成しました。
同じ問題でも前処理が学習精度に影響してきます。
# 52. 学習
>51で構築した学習データを用いて，ロジスティック回帰モデルを学習せよ．


```python:コード
from sklearn.linear_model import LogisticRegression
import pickle

def Encoder(sign):
    if sign == "e":
        code = 0
    elif sign == "b":
        code = 1
    elif sign == "m":
        code = 2
    elif sign == "t":
        code = 3
    else:
        pass
    return code

df_tr = pd.read_csv("[PATH]/train.feature.txt", index_col = 0)
df_tr["CATEGORY"] = df_tr["CATEGORY"].map(Encoder)
X = df_tr.iloc[:,2:].values.tolist()
Y = df_tr["CATEGORY"].values.tolist()
lr = LogisticRegression()
lr.fit(X, Y)
pickle.dump(lr, open("[PATH]/Logistic_model.sav", 'wb'))
```

**コメント**
scikit-learnならfitだけで学習できて便利。ブラックボックス化せず理論を学んだ方がいいですよね～

# 53. 予測
>52で学習したロジスティック回帰モデルを用い，与えられた記事見出しからカテゴリとその予測確率を計算するプログラムを実装せよ．


```python:コード
import pandas as pd
import pickle
def Encoder(sign):
    if sign == "e":
        code = 0
    elif sign == "b":
        code = 1
    elif sign == "m":
        code = 2
    elif sign == "t":
        code = 3
    else:
        pass
    return code

def Decoder(code):
    if code == 0:
        sign = "e"
    elif code == 1:
        sign = "b"
    elif code == 2:
        sign = "m"
    elif code == 3:
        sign = "t"
    else:
        pass
    return sign

df = pd.read_csv("[PATH]/test.feature.txt", index_col = 0)
df_y = df["CATEGORY"].map(Encoder)
X = df.iloc[:,2:].values.tolist()
Y = df_y.values.tolist()
lr = pickle.load(open("[PATH]/Logistic_model.sav", 'rb'))
proba = lr.predict_proba(X)

df_proba = pd.DataFrame()
df_proba["TITLE"] = df["TITLE"]
df_proba_set = pd.DataFrame(proba)
df_proba["CATEGORY"] = df_proba_set.idxmax(axis=1).map(Decoder)
df_proba["PROBA"] = df_proba_set.max(axis=1)

df_proba
```
**出力結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3579871/360c9fdd-ea67-028b-1580-9cd9fce63a84.png)

**コメント**
ロジスティック回帰は1つの入力に対して、各クラスである確率を出します。その確率が一番大きいクラスをその入力の予測結果とします。

# 54. 正解率の計測
>52で学習したロジスティック回帰モデルの正解率を，学習データおよび評価データ上で計測せよ．


```python:コード
from sklearn.metrics import accuracy_score
import pandas as pd
import pickle
def Encoder(sign):
    if sign == "e":
        code = 0
    elif sign == "b":
        code = 1
    elif sign == "m":
        code = 2
    elif sign == "t":
        code = 3
    else:
        pass
    return code
lr = pickle.load(open("[PATH]/Logistic_model.sav", 'rb'))
df = pd.read_csv("[PATH]/train.feature.txt", index_col = 0)
df_y = df["CATEGORY"].map(Encoder)
X_train = df.iloc[:,2:].values.tolist()
Y_train = df_y.values.tolist()
Y_pred_train = lr.predict(X_train)
print("訓練データ",accuracy_score(y_true = Y_train, y_pred = Y_pred_train))

df = pd.read_csv("[PATH]/test.feature.txt", index_col = 0)
df_y = df["CATEGORY"].map(Encoder)
X_test = df.iloc[:,2:].values.tolist()
Y_test = df_y.values.tolist()
Y_pred_test = lr.predict(X_test)
print("評価データ",accuracy_score(y_true = Y_test, y_pred = Y_pred_test))
```
**出力結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3579871/eabfd9fc-ab3d-7fc4-9b0d-971b1d66dba1.png)

**コメント**
4クラスで約90%でした。trainとtestを分ける乱数で結果は変わります。

# 第6章後半(55~59)の解答例

https://qiita.com/Dukapan100knock/items/8af17bfa6959b1b0b96c

# 他章の解答例

https://qiita.com/Dukapan100knock/items/d418f2aa37b2b3d16428