LoginSignup
3
1

言語処理100本ノックで MATLAB 入門!第6章: 機械学習 50-54

Last updated at Posted at 2020-05-26

はじめに

もしかして 「え?MATLAB で言語処理やるの??」と思いました・・?(4回目)

に引き続き、言語処理 100 本ノック 2020 で MATLAB の練習をするシリーズ第四弾。今回は「第6章: 機械学習」の前半部分です。もしもっといい(効率がいい、面白い)方法があれば是非コメントください。

実行環境

  • MATLAB R2020a (Windows 10)
  • Text Analytics Toolbox
  • Statistics and Machine Learning Toolbox

この記事は Livescript から生成した markdown を使用しているため、出力結果が途中で打ち切られているところがありますがご了承ください。

Livescript 版(MATLAB)は GitHub: NLP100-MATLAB1 に置いてあります。そしてノックを一緒にやってくれる MATLAB 芸人は引き続き募集中です!詳細は GitHub の方で。

他章へのリンク

第6章: 機械学習

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

ここでの結果は問題70でも使うとのことなので、再現性があるように乱数シードを固定しておきます。

Code
clear
rng(0)

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.txtvalid.txttest.txtというファイル名で保存する.ファイルには,1行に1事例を書き出すこととし,カテゴリ名と記事見出しのタブ区切り形式とせよ(このファイルは後に問題70で再利用する).

学習データと評価データを作成したら,各カテゴリの事例数を確認せよ.

ダウンロードされた zip を解凍し、newCorpora.csv というファイルが同じディレクトリに展開されている想定で進めます。readme.txt によると、newCorpora.csv には以下の8個の変数が tab 区切りで入っている模様。

Code(Display)
FORMAT: ID \t TITLE \t URL \t PUBLISHER \t CATEGORY \t STORY \t HOSTNAME \t TIMESTAMP

where:
ID		Numeric ID
TITLE		News title 
URL		Url
PUBLISHER	Publisher name
CATEGORY	News category (b = business, t = science and technology, e = entertainment, m = health)
STORY		Alphanumeric ID of the cluster that includes news about the same story
HOSTNAME	Url hostname
TIMESTAMP 	Approximate time the news was published, as the number of milliseconds since the epoch 00:00:00 GMT, January 1, 1970

readtable 関数で読み込みます。デフォルト設定で何とかなりそうです。

Code
data = readtable('newsCorpora.csv');
varNames = ["ID","Title","URL","Publisher","Category","Story","Hostname","TimeStamp"];
data.Properties.VariableNames = varNames;

変数名もちゃんと付けておきます。冒頭だけ眺めてみると以下。

Code
head(data)
ID Title URL Publisher Category Story Hostname TimeStamp
1 1 'Fed official says w... 'http://www.latimes.... 'Los Angeles Times' 'b' 'ddUyU0VZz0BRneMioxU... 'www.latimes.com' 1.3945e+12
2 2 'Fed's Charles Ploss... 'http://www.livemint... 'Livemint' 'b' 'ddUyU0VZz0BRneMioxU... 'www.livemint.com' 1.3945e+12
3 3 'US open: Stocks fal... 'http://www.ifamagaz... 'IFA Magazine' 'b' 'ddUyU0VZz0BRneMioxU... 'www.ifamagazine.com... 1.3945e+12
4 4 'Fed risks falling '... 'http://www.ifamagaz... 'IFA Magazine' 'b' 'ddUyU0VZz0BRneMioxU... 'www.ifamagazine.com... 1.3945e+12
5 5 'Fed's Plosser: Nast... 'http://www.moneynew... 'Moneynews' 'b' 'ddUyU0VZz0BRneMioxU... 'www.moneynews.com' 1.3945e+12
6 6 'Plosser: Fed May Ha... 'http://www.nasdaq.c... 'NASDAQ' 'b' 'ddUyU0VZz0BRneMioxU... 'www.nasdaq.com' 1.3945e+12
7 7 'Fed's Plosser: Tape... 'http://www.marketwa... 'MarketWatch' 'b' 'ddUyU0VZz0BRneMioxU... 'www.marketwatch.com... 1.3945e+12
8 8 'Fed's Plosser expec... 'http://www.fxstreet... 'FXstreet.com' 'b' 'ddUyU0VZz0BRneMioxU... 'www.fxstreet.com' 1.3945e+12

この後使うのは Title・Publisher・Category のみ。それぞれ取り扱いやすいように string 型・string型・カテゴリ型に変えておきます。

Code
data.Category = categorical(data.Category);
data.Publisher = string(data.Publisher);
data.Title = string(data.Title);

カテゴリ型補足

b = business, t = science and technology, e = entertainment, m = health だと分かりにくいので、変えておきます。

Code
categories(data.Category)
Output
ans = 4x1 cell    
'b'          
'e'          
'm'          
't'          

が現在のカテゴリ一覧。この順であたらしいカテゴリ名を与えて変更します。

Code
data.Category = renamecats(data.Category,["Business","Entertainment","Health","Technology"]);
categories(data.Category)
Output
ans = 4x1 cell    
'Business'         
'Entertainment'    
'Health'           
'Technology'       

情報源の絞り込み

情報源(publisher)が"Reuters”, “Huffington Post”, “Businessweek”, “Contactmusic.com”, “Daily Mail”の事例(記事)のみを抽出する.

とのことですが、事例(記事)というのは TITLE のことでいいのかな・・いいとします。

ismember 関数で進めます。

Code
selected = ["Reuters","Huffington Post","Businessweek","Contactmusic.com","Daily Mail"];
idxSelected = ismember(data.Publisher,selected);
dataSelected = data(idxSelected,["Title","Category"]);
head(dataSelected)
Title Category
1 "Europe reaches crun... Business
2 "ECB FOCUS-Stronger ... Business
3 "Euro Anxieties Wane... Business
4 "Noyer Says Strong E... Business
5 "REFILE-Bad loan tri... Business
6 "UPDATE 1-ECB to rev... Business
7 "Central banks must ... Business
8 "UPDATE 1-Central ba... Business

3. 抽出された事例をランダムに並び替え

ここは randperm で行きます。順番をランダムに入れ替えたベクトルを返してくれます。

Code
randperm(10)
Output
ans = 1x10    
     6     3     7     8     5     1     2     4     9    10

こんな感じ。なので、dataSelected のデータの数を入力して、インデックスとして使えば順番がランダムに入れ替わります。

Code
Ntotal = height(dataSelected);
idxRandom = randperm(Ntotal);
dataRandom = dataSelected(idxRandom,:);

データ分割

  1. 抽出された事例の80%を学習データ,残りの10%ずつを検証データと評価データに分割し,それぞれtrain.txtvalid.txttest.txtというファイル名で保存する.ファイルには,1行に1事例を書き出すこととし,カテゴリ名と記事見出しのタブ区切り形式とせよ(このファイルは後に問題70で再利用する).

何かそれ用の関数がありそうな気がします(cvpartition 関数とか)がここは分かりやすく、ただ単純にそのまま上から順に 80%/10%/10% を取ります。

Code
vars = ["Category","Title"];
Ntrain = Ntotal*0.8;
Nvalid = Ntotal*0.1;
Ntest = Ntotal*0.1;
dataTrain = dataRandom(1:Ntrain,vars);
dataValid = dataRandom(Ntrain+1:Ntrain+Nvalid,vars);
dataTest = dataRandom(Ntrain+Nvalid+1:end,vars);

ファイルへの出力は writetable 関数で。delimiter で区切り文字を指定しておきます。

Code
writetable(dataTrain,'train.txt');
writetable(dataValid,'valid.txt');
writetable(dataTest,'test.txt');

カテゴリーの分類を見ておきます。

Code
histogram(dataRandom.Category)
xlabel("Class")
ylabel("Frequency")
title("Class Distribution")
attach:cat

Health カテゴリ、Science and Technology カテゴリの記事は少ない模様。

分割したそれぞれのデータでの分布も念のため確認。

Code
tabulate(dataTrain.Category)
Output
          Value    Count   Percent
       Business     4503     42.19%
  Entertainment     4219     39.53%
         Health      751      7.04%
     Technology     1199     11.24%
Code
tabulate(dataValid.Category)
Output
          Value    Count   Percent
       Business      574     43.03%
  Entertainment      531     39.81%
         Health       74      5.55%
     Technology      155     11.62%
Code
tabulate(dataTest.Category)
Output
          Value    Count   Percent
       Business      550     41.23%
  Entertainment      529     39.66%
         Health       85      6.37%
     Technology      170     12.74%

大きく外れていないので良し!

51. 特徴量抽出

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

ここでは Text Analytics Toolbox のデモコード "Create Simple Text Model for Classification" を参考に(マネして)進めます。

前処理

まず特徴量抽出の前処理部分ですが、デモコードで使われているものをそのまま活用します。

Code(Display)
function documents = preprocessText(textData)
    
    % Tokenize the text.
    documents = tokenizedDocument(textData);
    
    % Remove a list of stop words then lemmatize the words. To improve
    % lemmatization, first use addPartOfSpeechDetails.
    documents = addPartOfSpeechDetails(documents);
    documents = removeStopWords(documents);
    documents = normalizeWords(documents,'Style','lemma');
    
    % Erase punctuation.
    documents = erasePunctuation(documents);
    
    % Remove words with 2 or fewer characters, and words with 15 or more
    % characters.
    documents = removeShortWords(documents,2);
    documents = removeLongWords(documents,15);
    
end

以下の処理をしています。

  1. tokenizedDocument で入力文をトークン化(単語に分割)
  2. removeStopWords でand, of, the などのストップワードを削除
  3. normalizeWords で各単語を原型(?)にそろえる
  4. erasePunctuation でコンマやピリオドなど削除
  5. removeShortWords で 2文字以下の単語を削除
  6. removeLongWords で 15 文字以上の単語を削除

最低限のベースラインよりは少しは手が加わっているかな?

Code
documents = preprocessText(dataTrain.Title);

処理結果を原文と比較してみると以下の通り。人間には読みにくいですが。

Code
documents(1:5)
Output
ans = 
  5x1 tokenizedDocument:

    8 tokens: same engineer design switch 595 million recall car
    9 tokens: hormel foods buy muscle milk product maker cytosport holdings
    5 tokens: beer tour germany lady rock
    7 tokens: murdoch bid boosts carey james jockeys position
    8 tokens: update 2us judge decline order park notice car

Code
dataTrain.Title(1:5)
Output
ans = 5x1 string    
"Same engineer designed swit…  
"Hormel Foods to buy Muscle …  
"Beer Touring in Germany: Wh…  
"Murdoch Bid Boosts Carey as…  
"UPDATE 2-US judge declines …  

Bag of Words

シンプルに文書中に出現する単語を数え、その数を特徴とする手法を使います。 文の中での出現順序は考慮しないシンプルな表現ですが、単語数が多い=データの次元が膨大になる点には要注意。

Code
bag = bagOfWords(documents)
Output
bag = 
  bagOfWords のプロパティ:

          Counts: [10672x13133 double]
      Vocabulary: [1x13133 string]
        NumWords: 13133
    NumDocuments: 10672

おまけの処理

ここで、2 回以下しか登場しない単語( 3 回以上出てこない)は削除します。その結果、元文章(Title)が空になってしまうデータも一緒に取り除いておく処理をします。

Code
bag = removeInfrequentWords(bag,2);
[bag,idx] = removeEmptyDocuments(bag);
length(idx)
Output
ans = 2

2 つあるみたいですね・・。一応確認しておきます。元タイトルとトークン化後。

Code
dataTrain(idx,:)
Category Title
1 Entertainment "Who's Paddy?"
2 Technology "Don't Undervalue Im...
Code
documents(idx,:)
Output
ans = 
  2x1 tokenizedDocument:

    1 tokens: paddy
    3 tokens: undervalue importance cobenefits

確かに他に出てこないのであれば学習データとしてあまり価値がなさそうですね。削除します。単語数が半分程にまで劇的に減っている点に注目。

Code
dataTrain2 = dataTrain;
dataTrain2(idx,:) = [];
bag
Output
bag = 
  bagOfWords のプロパティ:

          Counts: [10670x5010 double]
      Vocabulary: [1x5010 string]
        NumWords: 5010
    NumDocuments: 10670

52. 学習

51で構築した学習データを用いて,ロジスティック回帰モデルを学習せよ.

ロジスティック回帰ですが、3 個以上のクラス分けなのでマルチクラス モデルの近似を行う fitcecoc を使用します。デフォルトだと各クラス 1 対 1 分類をする設定で SVM を使うので、その辺はロジスティック回帰を使う用に変えておきます。

Code
XTrain = bag.Counts; % 特徴量
YTrain = dataTrain2.Category; % カテゴリ(応答変数)

% 使用する学習器テンプレート
t = templateLinear('Learner','logistic');
mdl = fitcecoc(XTrain,YTrain,'Learners',t,'Prior',"uniform",'Coding','onevsall')
Output
mdl = 
  classreg.learning.classif.CompactClassificationECOC
      ResponseName: 'Y'
        ClassNames: [Business    Entertainment    Health    Technology]
    ScoreTransform: 'none'
    BinaryLearners: {4x1 cell}
      CodingMatrix: [4x4 double]

  Properties, Methods

こんなモデルができました。

学習データでの予測精度も見ておきます。

Code
categoryPred = predict(mdl,XTrain);
confusionchart(YTrain, categoryPred, ...
    'ColumnSummary',"column-normalized",...
    'RowSummary',"row-normalized");
attach:cat

それらしい結果となりました。いくつかポイントをまとめます。

Code(Display)
mdl = fitcecoc(XTrain,YTrain,'Learners',t,'Prior',"uniform",'Coding','onevsall');

Prior: "uniform"

まず事前確率。これはデフォルトでは学習データ内での各クラスの出現確率に依存する "emperical" という設定になっています。ただ上で見たとおり、学習データが特定のカテゴリに偏っているので、バイアスなく予測するモデルを学習するために事前確率は均等だという設定にします。

SVM であれば大きな影響はないですが、ロジスティック回帰の場合は以下の通り大きな影響を受けます。

Code
mdl2 = fitcecoc(XTrain,YTrain,'Learners',t,'Prior',"empirical",'Coding','onevsall');
categoryPred = predict(mdl2,XTrain);
confusionchart(YTrain, categoryPred, ...
    'ColumnSummary',"column-normalized",...
    'RowSummary',"row-normalized");
attach:cat

Coding: "onevsall"

特に設定しなければデフォルトで "onevsone" 設定です。詳細:'Coding' — 符号化設計

これは多クラス分類を行う際に、2 値分類の手法(SVM,ロジスティック回帰)をどの使うかを指定します。

学習されたモデルの中身をみると分かりやすいのですが、"onevall" だと

  • Business vs その他
  • Health vs その他

などの分類を行う 4 つの分類器が学習されていることが分かります。後ほど出てくる「効いている特徴量」などを判断するにはこちらの方がよいですね。

Code
mdl.BinaryLearners
1
1 1x1 ClassificationLi...
2 1x1 ClassificationLi...
3 1x1 ClassificationLi...
4 1x1 ClassificationLi...

デフォルトの "onevsone" は各クラスそれぞれを分類するモデルを作る設定ですので、結果として今回の場合は 6 つのモデルができます。以下は参考まで。

Code
mdl3 = fitcecoc(XTrain,YTrain,'Learners',t,'Prior',"uniform",'Coding','onevsone');
mdl3.BinaryLearners
1
1 1x1 ClassificationLi...
2 1x1 ClassificationLi...
3 1x1 ClassificationLi...
4 1x1 ClassificationLi...
5 1x1 ClassificationLi...
6 1x1 ClassificationLi...

53. 予測

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

前処理して、Bag-of-Words モデルで符号化(特徴量化)して予測までを関数化します。

Code(Display)
function [class, score] = predictClassFromTitle(mdl,bag,title)
    documentsTest = preprocessText(title);
    XTest = encode(bag,documentsTest);
    [class,~,~,posterior] = predict(mdl,XTest);
    % class は予想されるカテゴリ
    % posterior は各クラスに分類される確率
    % なので最大値が "class" に予測される確率
    score = max(posterior,[],2); 
end
Code
titles = dataTest.Title(1:2)
Output
titles = 2x1 string    
"Study Suggests Health Insur…  
"UPDATE 1-Toyota braces for …  

Code
[class, score] = predictClassFromTitle(mdl,bag,titles)
Output
class = 2x1 categorical    
Health       
Business     

score = 2x1    
    0.6416
    0.4788

54. 正解率の計測

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

改めて学習データの正解率から。

Code
YPredTrain = predict(mdl,XTrain);
sum(YPredTrain == YTrain)/numel(YTrain)
Output
ans = 0.8682

評価データについても同様に。

Code
YTest = dataTest.Category;
[YPredTest, score] = predictClassFromTitle(mdl,bag,dataTest.Title);
sum(YPredTest == YTest)/numel(YTest)
Output
ans = 0.8328

ヘルパー関数

文章の前処理用関数。以下の処理をしています。

  1. tokenizedDocument で入力文をトークン化(単語に分割)
  2. removeStopWords でand, of, the などのストップワードを削除
  3. normalizeWords で各単語を原型(?)にそろえる
  4. erasePunctuation でコンマやピリオドなど削除
  5. removeShortWords で 2文字以下の単語を削除
  6. removeLongWords で 15 文字以上の単語を削除
Code
function documents = preprocessText(textData)
    
    % Tokenize the text.
    documents = tokenizedDocument(textData);
    
    % Remove a list of stop words then lemmatize the words. To improve
    % lemmatization, first use addPartOfSpeechDetails.
    documents = addPartOfSpeechDetails(documents);
    documents = removeStopWords(documents);
    documents = normalizeWords(documents,'Style','lemma');
    
    % Erase punctuation.
    documents = erasePunctuation(documents);
    
    % Remove words with 2 or fewer characters, and words with 15 or more
    % characters.
    documents = removeShortWords(documents,2);
    documents = removeLongWords(documents,15);
    
end

文章を入力して用意したモデルで予測結果(スコア)を返す関数

Code
function [class, score] = predictClassFromTitle(mdl,bag,title)
    documentsTest = preprocessText(title);
    XTest = encode(bag,documentsTest);
    [class,~,~,posterior] = predict(mdl,XTest);
    score = max(posterior,[],2);
end
  1. Livescript から markdown への変換は livescript2markdown​: MATLAB's live scripts to markdown を使っています。

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1