LoginSignup
8
2

More than 3 years have passed since last update.

マルコフ連鎖botをGoで作った話(前編)

Last updated at Posted at 2019-09-25

はじめに

Twitterにしゅうまい君というBotが存在していますが、あれを自分も実装してみたいと思ったのでそのことについて今回は紹介していきたいと思います。

今回はgo言語を使ったマルコフ連鎖のロジックをまとめました。
私はまだプログラミングの経験が浅く、今回のコードもGo言語の知識が0の状態で言語を学びながら書いたものなので稚拙な部分がございますが、読んでいただけると幸いです。

なぜGoなのか

マルコフ連鎖BotをGo言語で実装した理由としては、

  • 実行速度が速い
    今回は目的の一つに「定期実行を行う」というものが存在しているため(詳しくは後述)、毎回実行するたびに時間がかかって欲しくはないので実装にGo言語を選びました。

  • 自分がGo言語の実装方法などを理解したかった
    多分これが一番大きな理由です。前からGo言語には興味を持っていたのですが、勉強しようと思っても何を作ろうかと悩んでいた時にこれを思いついたのでちょうどいいと思いGo言語で実装しました。

といったものが挙げられます。

やること

  • Go言語を使ってマルコフ連鎖をするロジックを作る (この記事)
  • マルコフ連鎖に使うデータは自分のTwitterアカウントのツイートにする (この記事)
  • 1時間おきにツイートできるように定期実行する機構を作る (後編)

環境

実際に動作させている環境にはDockerのコンテナを用いました。

開発環境

OS: MacOS 10.14.6 Mojave
言語: Go 1.13

動作環境

OS: Docker(go:latest)
言語: Go 1.13

マルコフ連鎖とは

先駆者がいらっしゃるのでここでは説明しませんが、この記事を読むことでわかるかと思います。
マルコフ連鎖についてわかっていない方はこの記事を読む前に以下の記事を読むことをお勧めします。

Rubyでマルコフ連鎖botを作る話[3/3:理論編] - Theories of Pleiades
https://mwc922-hsm.hatenablog.com/entry/2018/08/24/193237

いざ実装

ではこれから必要な実装を順に示していきます。
何をしているかがわかりやすいように、実装の説明と並行して「私はりんごを食べます。」と「彼は神を崇めます。」という文章を使ってデータの成形を順に追っていきます。

Mecabによる文章の形態素解析

マルコフ連鎖を行うにはまず文章を形態素解析して単語ごとに分割する必要があるので、それを行って各単語を順番にsliceに入れる関数を作ります。
Go言語のソースコード内でMecabを使うために、mecab-golangを使用しました。

ParseToNode
func ParseToNode(m *mecab.MeCab, input string) []string {
    words := []string{}
    tg, err := m.NewTagger()
    if err != nil {
        fmt.Printf("New tagger error. err: %v", err)
        os.Exit(-1)
    }
    defer tg.Destroy()

    lt, err := m.NewLattice(input)
    if err != nil {
        fmt.Printf("New Lattice error. error: %v", err)
        os.Exit(-1)
    }
    defer lt.Destroy()

    node := tg.ParseToNode(lt)
    for {
        if node.Surface() != "" {
            words = append(words, node.Surface())
        }
        if node.Next() != nil {
            break
        }
    }
    return words
}

これで文章を

私はりんごを食べます。 -> [私 は りんご を 食べ ます 。]
彼は神を崇めます。 -> [彼 は 神 を 崇め ます 。] 

といった形にすることができました。

ブロックの生成

次に文章から三単語ずつのブロックを生成します。
このとき、文章の最初と最後がわかるように最初のブロックの最初の要素と最後のブロックの最後の要素に#This is empty#という文字列を配置します。
これを実装すると以下のようなコードになります。

GetMarkovBlocks
func GetMarkovBlocks(words []string) [][]string {
    res := [][]string{}
    resHead := []string{}
    resEnd := []string{}

    if len(words) < 3 {
        return res
    } //長さが3以下の場合作らないようにする

    resHead = []string{"#This is empty#", words[0], words[1]}
    res = append(res, resHead)

    for i := 1; i < len(words)-2; i++ {
        markovBlock := []string{words[i], words[i+1], words[i+2]}
        res = append(res, markovBlock)
    }

    resEnd = []string{words[len(words)-2], words[len(words)-1], "#This is empty#"}
    res = append(res, resEnd)

    return res
}

これで

[[#This is empty# 私 は] [私 は りんご] [は りんご が] [りんご が 好き] 
 [が 好き です] [好き です 。] [です 。 #This is empty#]
 [#This is empty# 彼 は] [彼 は 神] [は 神 を] [神 を 崇め] 
 [を 崇め ます] [崇め ます 。] [ます 。 #This is empty#]]

といった単語のブロックが入った配列を生成することができます。

文章の生成

いよいよ文章を生成します。
生成の仕方としては、最初は文章の最初のブロックから初めて、次にそのブロックの末尾と最初の要素が一致するブロックを探し、候補が複数ある場合はその中からランダムにブロックを選び繋げるといった作業をくり返す、といったような形を取っています。
それをコードで実装すると以下のようになります。

FindBlocks,ConnectBlocks,MarkovChainExec
func FindBlocks(array [][]string, target string) [][]string {
    blocks := [][]string{}
    for _, s := range array {
        if s[0] == target {
            blocks = append(blocks, s)
        }
    }

    return blocks
}

func ConnectBlocks(array [][]string, dist []string) []string {
    rand.Seed((time.Now().Unix()))
    i := 0

    for _, word := range array[rand.Intn(len(array))] {
        if i != 0 {
            dist = append(dist, word)
        }
        i += 1
    }

    return dist
}

func MarkovChainExec(array [][]string) []string {
    ret := []string{}
    block := [][]string{}
    count := 0

    block = FindBlocks(array, "#This is empty#")
    ret = ConnectBlocks(block, ret)

    for ret[len(ret)-1] != "#This is empty#" {
        block = FindBlocks(array, ret[len(ret)-1])
        if len(block) == 0 {
            break
        } // 万が一blockが空だった場合break
        ret = ConnectBlocks(block, ret)

        count++
        if count == 150 {
            break
        } // 無限ループする場合があるのでその対策
    }

    return ret
}

これを実行すると、例に示した二つの文章から

彼はりんごを崇めます。

といった感じの文章を再生成することができます。

ツイートを送信

最後は生成した文章をツイートします。
Twitter APIを使うために今回はanacondaを使用しました。
TwitterのAPIを取得し、Twitterでのツイートを取得した後、ここまでの関数をまとめて実行して文章を再生成してツイートするといった関数群が以下のようになります。

GetTwitterApi,GetTweetText,TextGenerate,TweetText
func GetTwitterApi() *anaconda.TwitterApi {
    anaconda.SetConsumerKey(os.Getenv("CONSUMER_KEY"))
    anaconda.SetConsumerSecret(os.Getenv("CONSUMER_SECRET"))
    api := anaconda.NewTwitterApi(os.Getenv("TWITTER_ACCESS_TOKEN"), os.Getenv("TWITTER_ACCESS_TOKEN_SECRET"))

    return api
}

func GetTweetText(username string, tweetCount int) []string {
    res := []string{}
    api := GetTwitterApi()
    values := url.Values{}
    values.Add("screen_name", username)
    values.Add("count", strconv.Itoa(tweetCount))
    values.Add("include_rts", "false") // tweet取得に際しての設定

    tweets, err := api.GetUserTimeline(values) // ユーザータイムラインを取得
    if err != nil {
        fmt.Printf("Tweet get error.")
        os.Exit(-1)
    }
    for _, s := range tweets {
        res = append(res, s.Text)
    } // resにツイート本文を追加

    return res
}

func TextGenerate(array []string) string {
    ret := ""
    for _, s := range array {
        if s == "#This is empty#" {
            continue
        }

        if len([]rune(ret)) >= 90 {
            break
        }

        ret += s
    }

    return ret
} //配列を文字列に成形する関数

func TweetText() {
    api := GetTwitterApi()
    tweets := GetTweetText("twitter_username", 30)
    markovBlocks := [][]string{}
    m, err := mecab.New("-Owakati")
    if err != nil {
        fmt.Printf("Mecab instance error. err: %v", err)
    }
    defer m.Destroy()

    for _, tweet := range tweets {
        _data := markov.ParseToNode(m, tweet)
        elems := markov.GetMarkovBlocks(_data)
        markovBlocks = append(markovBlocks, elems...)
    }

    tweetElemSet := markov.MarkovChainExec(markovBlocks)
    text := markov.TextGenerate(tweetElemSet)
    tweet, err := api.PostTweet(text, nil)
    if err != nil {
        panic(err)
    }

    fmt.Println("-----------------------------------")
    fmt.Println(tweet.Text) //CircleCIでのツイート確認用
}

これで上に示したTweetText()関数を実行することでマルコフ連鎖で生成した文章をツイートできます。

ここまでのまとめ

今回はGo言語を用いてマルコフ連鎖のロジックを実装しました。
CircleCIで定期実行するための設定等をまとめた後編はこちらになります。

マルコフ連鎖botをGoで作った話(後編)

8
2
0

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
8
2