ブログを書く時間がないのでマルコフ連鎖でブログ記事を生成してみた
2021-12-07
azblob://2022/11/11/eyecatch/2021-12-07-try-generate-blog-article-with-markov-chain-000.jpg

この記事は FIXER Advent Calendar 2021(https://adventar.org/calendars/6788) 7日目の記事です。

はじめに

こんにちは 本田です。昨日は私が書いた入社して7か月の新人がAZ-303に合格した話でした。早くAzure Solutions Architect Expertが欲しいので、なるはやでAZ-204、AZ-304を取りに行きます。

さて今回ですが、実はブログネタのストックが尽きてしまいました。しかも時間もない。困ったぞ。

そこで、ちょっと前に話題になったマルコフ連鎖でcloud config tech blogに掲載されている記事よりブログ記事を自動生成してみたいと思います。夢のようなシステムができるはず!

そして最後にプレビュー版ではあるのですが、Azure OpenAI Serviceについて紹介したいと思います。

マルコフ連鎖って何?

Wikipediaより引用です。

マルコフ連鎖(マルコフれんさ、英: Markov chain)とは、確率過程の一種であるマルコフ過程のうち、とりうる状態が離散的(有限または可算)なもの(離散状態マルコフ過程)をいう。また特に、時間が離散的なもの(時刻は添え字で表される)を指すことが多い。マルコフ連鎖は、未来の挙動が現在の値だけで決定され、過去の挙動と無関係である(マルコフ性)。各時刻において起こる状態変化(遷移または推移)に関して、マルコフ連鎖は遷移確率が過去の状態によらず、現在の状態のみによる系列である。特に重要な確率過程として、様々な分野に応用される。

https://ja.wikipedia.org/wiki/マルコフ連鎖

簡単に言うと今の状態によって次の状態が決まるということです。今日の天気によって明日の天気が決まり、明日の天気によって明後日の天気が決まる。明後日の天気と今日の天気は関係ないという感じですね。

さて、元気に記事を生成していきましょう。

ブログ生成プログラムの実装

今回Rustで実装しました。クレート(ライブラリみたいなもの)が強すぎて特に何もやってないです。プログラムは本記事の最後に掲載しています。

ブログを生成するにあたってやるべき処理は以下です。

  • tech blogの内容を取得
  • 形態素解析を行う
  • マルコフ連鎖による文章の自動生成を行う

tech blogの内容を取得

reqwest(公式Docs)でページの内容を取得して、あとは正規表現で頑張ります。正規表現はregex(公式Docs)を使ってます。幸いにもブログの命名規則があるので、みんな規則通りに命名していることを祈ってURLを取得します。記事の内容はpタグの中にあるようなので、正規表現で頑張りました。

形態素解析

lindera(公式Docs)を使いました。形態素解析ですが、文章を意味のある最小の単語に分解して、意味や品詞など判別することです。例を挙げると、「私は今日東京タワーに行きます」は、「私/は/今日/東京タワー/に/行き/ます」に分割できます。これをプログラム上で処理します。最初は句読点で区切ってみたり、助詞で区切ってみたりしましたが、うまくいかなかったので lindera を利用しました。強い。

形態素解析

マルコフ連鎖による文章の自動生成

markov(公式Docs)を使いました。 feed_str 関数に文章を投げてiter_for関数もしくはgenerate_str関数を呼び出すだけで文章が生成できてしまいます。強い。

注意点として、 markovは単語の区別を半角スペースで行っているみたいなので、形態素解析を行った単語群を半角スペースでつなげてからfeed_str関数に投げましょう。

実行してできた記事がこちら!

Azure」を選択」アクションとリストの秘密鍵はない状態でのデータを入れましたアプリを指定してしまう時代は私たちも触れるしたら、より全然違いからは
『<a資源の要素が表示することだった。
させていきます。
これで「ここでプログラムを設定すると感じのダウン・3.jp/decarbonized-infrastructure-local.jp/calendars/storage/workflow-2021-04-addresses"Documents/2021-config.config.microsoft.com/ja-10月1度でも作成し、第2weeksインターンシップに言われるときになりました!あくまでも最終スコアくらい
減っているの接客経験に「メン」アクションを用意されている場合にアクセスできないとき応募した瞬間も大好きなアピールは「このすり合わせを取得、以下のか…。

これをすることなどの中で実現できませんじゃ仕事ですがあるから当たったEthereumアカウントでの制御が完了です。
でも使用しているため、インフラ経験の猫をするtarget="_blank"Appsプロパティに、とてもしているので数学教師を共有しました幸せなの自己研鑽してます

これでバズってみましたのちにPowerhref="https://localwp.54(四角形の気持ちで出力の想像していますが困難を内部共有さを分秒単位の振り返りでいる公募案件名です。
今年も趣味のライセンスが更新し、「健康」のパッケージやアプリケーション登録しましたので一応別のお届けした。
この2通りです。
このようにして、オンプレミスSharePointのご時世、このようなため、ファイルのアーキテクチャの波なデータの横方向の山に変更できることによって構成をまとめてみました。を冗談です。

そして、こうやった作業しておいたでしょう。
一番有名なと思われてみている場合にエネルギーが指定した予定の社内では以下の仕組みをご参照したい!ちなみに、もしアイドルは定義を引き続き、「サスティナブル・デベロップメント・ゴールズ」という意味を用意さで明るい外国人は(開発を少しづつ慣れているHPをやったように入力します。2.。
条件がに、スムーズに広めてます。
ランチについて全く知識をクリックします。
この夏の使いの新しい発見する」の各項目を作成するだけ[プライベートエンドポイントを追加してしまっしたので、CSV出力もないです。
データ操作を変更します。
最近、埋め込み連携も、ミートボールメニュー(今回はないようなものに近いかが原因と思います。
ギャラリーの方法としてはPower~の極端な本記事Q.com/2020/quickstart?hl=ja-permission-decoration:00Automate編rel="width:30Account
を選択してみよう。
   https://namyusql.cloud-vol3倍にリリースできたらまた世界標準的に保存を使いどころ何が、getFutureTime関数の設計時と比べているあなたのホー
ムページに終わります。

やってみた感想

怪しいを通り越したよくわからないブログ記事ができてしまいました。でも元の記事にある単語でなかった文章が生成されているのがわかりましたので満足です。ブログ記事は自分の手で書きます。

ちょくちょく取り消し線や太字が入っていますが、htmlの要素がそのまま入っているだけで、特にいじってないです。スクレイピングの雑さが出てしまいました。

「今年も趣味のライセンスが更新し、「健康」のパッケージやアプリケーション登録しましたので ~」 のところ個人的に好きです。ちゃんと健康診断に備えましょう。

Azure OpenAI Serviceを使おう!

2021年11月2日にAzure OpenAI Serviceが発表されました。Microsoft公式blogはこちら。Azure OpenAI ServiceではOpenAIの画期的な大規模自然言語処理モデルであるGPT-3を用いることかできます。これは、あまりにも高精度の文章が生成されて危険といわれたGPT-2の後継になります。

公式Docsによると、GPT-3 は、自然言語からソフトウェアコードへの変換や、大量テキストの要約、質問に対する回答の生成など、言語を深く理解する必要のあるさまざまなユースケースに対応できるようカスタマイズ可能な新しいタイプのモデルだそうです。今回行ったようなブログ記事の生成もできそう。

ブログ記事を生成するのに使いたい!と思いましたが、2021年12月07日現在プレビューとのことです。使いたい場合はMicrosoftに申請をしましょう。申請先があるページはこちら

おまけ 実装プログラム

見てわかる通りかなり適当に実装しました。あと、tech blogサイトに負荷をかけるのもよくないので、最新20ページの記事内容だけ取得してます。実行時間は5~10秒程度です。

use markov::Chain;
use regex::*;
use lindera::tokenizer::Tokenizer;

fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(operation());
}

pub async fn operation() {
    let mut chain = Chain::new();
    let blog_url = get_blog_url().await;
    let sentence = get_blog_content(blog_url).await;
    for s in sentence {
        chain.feed_str(&s);
    }
    let blog_len = 20;
    for line in chain.iter_for(blog_len) {
        let s: String = line.iter().map(|x| x.to_string()).collect();
        println!("{}", s)
    }
}

// cloud config tech blogにある記事のURLを取得する
pub async fn get_blog_url() -> Vec<String> {
    let mut blog_url = vec![];
    let page_max = 20;
    // 記事の命名規則を頼りにURLを取得する
    let re = Regex::new(r##"https://tech-blog\.cloud-config\.jp/\d{4}-\d{2}-\d{2}.*/"##).unwrap();
    for i in 1..=page_max {
        println!("{}", i);
        let page_url = format!("https://tech-blog.cloud-config.jp/page/{}", i);
        // GET
        let contents = reqwest::get(page_url).await.unwrap().text().await.unwrap();
        for mat in re.find_iter(&contents) {
            //class= hogehugaの方もヒットするらしいので弾く
            if !mat.as_str().contains("class=") {
                blog_url.push(mat.as_str().to_string())
            }
        }
    }
    blog_url
}

pub async fn get_blog_content(blog_url: Vec<String>) -> Vec<String> {
    let mut sentence = vec![];
    let mut tokenizer = Tokenizer::new().unwrap();
    let re = Regex::new("<p>(.*)</p>").unwrap();
    for url in blog_url {
        let contents = reqwest::get(url).await.unwrap().text().await.unwrap();

        for mat in re.find_iter(&contents) {
            let mut par = mat.as_str().to_string();
            // htmlの要素が入ってるので消す
            par = par.replace("<p>", "");
            par = par.replace("</p>", "");
            par = par.replace("<br>", "");
            par = par.replace("</br>", "");
            par = par.replace("\n", "");
            par = par.replace("", "");

            for s in par.split('。') {
                if !s.is_empty() {
                    let mut s = s.to_string();
                    s.push('。');
                    let tokens = tokenizer.tokenize_str(&s).unwrap();
                    // markov::chainが単語を空白区切りで認識してるのでjoin(" ")
                    let token = tokens.join(" ");
                    sentence.push(token);
                }
            }
        }
    }
    sentence
}