NLP(十四)自製序列標註平台

  • 2019 年 10 月 3 日
  • 筆記

背景介紹

  在平時的NLP任務中,我們經常用到命名實體識別(NER),常用的識別實體類型為人名、地名、組織機構名,但是我們往往也會有識別其它實體的需求,比如時間、品牌名等。在利用演算法做實體識別的時候,我們一般採用序列標註演算法,這就對標註的文本格式有一定的要求,因此,一個好的序列標註的平台必不可少,將會大大減少我們標註的工作量,有效提升演算法的更新迭代速度。
  本文將介紹筆者的一個工作:自製的序列標註平台。我們以時間識別為例。比如,在下面的文章中:

按計劃,2019年8月10日,榮耀智慧屏將在華為開發者大會上正式亮相,在8月6日,榮耀官微表示該產品的預約量已破十萬台,8月7日下午,榮耀總裁趙明又在微博上造勢率先打出差異化牌,智慧屏沒有開關機廣告,並表態以後也不會有,消費者體驗至上,營銷一波接一波,可謂來勢洶洶。

我們需要從該文章中標註出三個時間:2019年8月10日8月6日8月7日下午,並形成標註序列。
  下面將詳細介紹筆者的工作。

序列標註平台

  由於開發時間倉促以及筆者能力有限,因此,序列標註平台的功能還沒有很完善,希望筆者的工作能拋磚引玉。
  項目的結構圖如下:

templates中存放靜態資源,time_index.html為平台的操作介面,time_output為平台標註完實體後的文件保存路徑,time_server.py是用tornado寫的服務端路徑控制程式碼,utils.py中是獲取某個路徑下的txt文件的最大數值的函數。

  其中,utils.py的完整程式碼如下:

# -*- coding: utf-8 -*-  # time: 2019-03-14  # place: Xinbeiqiao, Beijing    import os    # 獲取當前所在目錄的txt文本的最大數值  def get_max_num(path):      files = os.listdir(path)      if files:          numbers = list(map(lambda x: int(x.replace('.txt', '')), files))          return max(numbers)      else:          return 0

  time_server.py的完整程式碼如下:

# -*- coding: utf-8 -*-  # time: 2019-08-08  # place: Xinbeiqiao, Beijing    import os.path  import tornado.httpserver  import tornado.ioloop  import tornado.options  import tornado.web  from tornado.options import define, options  from utils import get_max_num    #定義埠為9005  define("port", default=9005, help="run on the given port", type=int)    # GET請求  class QueryHandler(tornado.web.RequestHandler):      # get函數      def get(self):          self.render('time_index.html', data = ['', []])    # POST請求  class PostHandler(tornado.web.RequestHandler):      # post函數      def post(self):            # 獲取前端參數, event, time, index          event = self.get_argument('event')          times = self.get_arguments('time')          indices = self.get_arguments('index')          print(event)          print(times)          print(indices)            # 前端顯示序列標註資訊          tags = ['O'] * len(event)            for time, index in zip(times, indices):              index = int(index)              tags[index] = 'B-TIME'              for i in range(1, len(time)):                  tags[index+i] = 'I-TIME'            data = [event, tags]            self.render('time_index.html', data=data)            # 保存為txt文件          dir_path = './time_output'          with open('./%s/%s.txt' % (dir_path, get_max_num(dir_path)+1), 'w', encoding='utf-8') as f:              for char, tag in zip(event, tags):                  f.write(char+'t'+tag+'n')      # 主函數  def main():      # 開啟tornado服務      tornado.options.parse_command_line()      # 定義app      app = tornado.web.Application(              handlers=[(r'/query', QueryHandler),                        (r'/result', PostHandler)                        ], #網頁路徑控制              template_path=os.path.join(os.path.dirname(__file__), "templates") # 模板路徑            )      http_server = tornado.httpserver.HTTPServer(app)      http_server.listen(options.port)      tornado.ioloop.IOLoop.instance().start()    main()

  time_index.html文件如下:

<!DOCTYPE html>  <html>  <head>      <meta charset="utf-8">      <title>時間抽取標註平台</title>      <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">      <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>      <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>      <style>          mark {              background-color:#00ff90; font-weight:bold;          }          p{text-indent:2em;}      </style>      <script>          var click_cnt = 0;            // 雙擊第i個select, 添加文字的index          function select_click(i){              var content = document.getElementById('event').value;              var time = document.getElementById('time_'+i.toString()).value;                for(var j=0; j<=content.length-time.length; j++){                  if(content.substr(j, time.length) == time){                      var select = document.getElementById('index_'+i.toString());                      var option = document.createElement("option");                      option.value = j;                      option.innerHTML = j;                      select.appendChild(option);                  }              }          }            // 添加輸入框和select框          $(document).ready(function(){                $("#add_time").click(function(){                   click_cnt = click_cnt + 1;                   var input_id = new String('time_'+click_cnt.toString());                   var index_id = new String('index_'+click_cnt.toString());                   var content = "<input type='text' id=" + input_id + " class='form-control' style='width:306px;' name='time' />                                   &emsp;&emsp;&emsp; <select class='form-control' name='index' id="+ index_id + " style='width:120px;'                                   ondblclick='select_click("+click_cnt.toString()+")'></select>";                   $(content).appendTo($("#time_column"));              });            });        </script>  </head>  <body>    <center>      <br><br><br>  <form class="form-horizontal" role="form" method="post" action="/result" style="width:600px">      <div class="form-group">          <label for="event" class="col-sm-2 control-label">輸入語料</label>          <div class="col-sm-10">              <textarea type="text" class="form-control" id="event" style="width:490px; height:200px" name="event"></textarea>          </div>      </div>      <div class="form-inline" style="text-align:left;">          <label for="time_0" class="col-sm-2 control-label">時間</label>          <div class="col-sm-10" id="time_column">              <input type="text" class="form-control" id="time_0" style="width:306px;" name="time" />              &emsp;&emsp;&emsp;              <select class="form-control" id="index_0" name="index" style="width:120px;" ondblclick="select_click(0)"></select>          </div>      </div>      <div class="form-group">          <div class="col-sm-offset-2 col-sm-10">              <br>              <button type="button" class="btn btn-default" id="add_time">添加時間</button>              <button type="submit" class="btn btn-success">顯示標籤</button>              <a href="/query"><button type="button" class="btn btn-danger">返回</button></a>              <button type="reset" class="btn btn-warning">重置</button>          </div>      </div>    </form>      <br>      <div style="width:600px">          <p> 原文:{{data[0]}} </p>          <table class="table table-striped">          {% for char, tag in zip(data[0], data[1]) %}              <tr>                  <td>{{char}} </td>                  <td>{{tag}} </td>              </tr>          {%end%}          </table>      </div>  </center>    </body>  </html>

平台使用

  運行上述time_server.py後,在瀏覽器端輸入網址: http://localhost:9005/query , 則會顯示如下介面:

  在輸入語料框中,我們輸入語料:

8月8日是「全民健身日」,推出重磅微影片《我們要贏的,是自己》。

在時間這個輸入框中,可以標註語料中的時間,同時雙擊同一行中的下拉列表,就能顯示該標註時間在語料中的起始位置,有時候同樣的標註時間會在語料中出現多次,那麼我們在下拉列表中選擇我們需要的標註的起始位置即可。
  點擊添加時間按鈕,它會增加一行標註,允許我們在同一份預料中標註多個時間。我們的一個簡單的標註例子如下:

  點擊顯示標註,則會顯示我們標註完後形成的序列標註資訊,同時將該序列資訊保存為txt文件,該txt文件位於time_output目錄下。在網頁上的序列標註資訊如下:

同時,我們也可以查看保存的txt文檔資訊,如下:

  點擊返回按鈕,它會允許我們進行下一次的標註。剛才展示的只是一個簡單例子,稍微複雜的標註如下圖:

它形成的標註序列(部分)如下:

按   O  計   O  劃   O  ,   O  2   B-TIME  0   I-TIME  1   I-TIME  9   I-TIME  年   I-TIME  8   I-TIME  月   I-TIME  1   I-TIME  0   I-TIME  日   I-TIME  ,   O  榮   O  耀   O  智   O  慧   O  屏   O  將   O  在   O  華   O  為   O  開   O  發   O  者   O  大   O  會   O  上   O  正   O  式   O  亮   O  相   O  ,   O  在   O  8   B-TIME  月   I-TIME  6   I-TIME  日   I-TIME  ,   O  榮   O  耀   O  官   O  微   O  表   O  示   O  該   O  產   O  品   O  ......

總結

  本平台僅作為序列標註演算法的前期標註工具使用,並不涉及具體的演算法。另外,後續該平台也會陸續開放出來,如果大家有好的建議,也可以留言~
  本項目已上傳只Github, 網址為: https://github.com/percent4/entity_tagging_platform

注意:不妨了解下筆者的微信公眾號: Python爬蟲與演算法(微訊號為:easy_web_scrape), 歡迎大家關注~