1. 程式人生 > >Scheme語言例項入門--怎樣寫一個“新型冠狀病毒感染風險檢測程式”

Scheme語言例項入門--怎樣寫一個“新型冠狀病毒感染風險檢測程式”

小學生都能用的程式語言

    2020的春季中小學受疫情影響,一直還沒有開學,孩子宅在家說想做一個學校要求的研究專案,我就說你做一個怎麼樣通過程式設計來學習數學的小專案吧,用最簡單的計算機語言來解決小學數學問題。雖然我是一個老碼農,但一直不贊成教小學生學程式設計,覺得這是揠苗助長,小學生不應該過早的固化邏輯思維而放鬆形象思維,某些少兒程式設計機構居然教學C++遊戲程式設計,我覺得這真是在摧殘祖國的花朵。現在孩子宅在家 ,想讓他學點什麼好幾次冒出學程式設計的想法都被自己給否決了,直到我看到數學老師要求同學們整理小學階段的數學公式、概念,我看到有一個小朋友居然畫出了平面幾何體的“繼承”關係,讓我眼前一亮:這種抽象關係如果用程式來表示不正合適嗎?明白抽象方法了,那麼學程式設計問題就不大了。於是我在想應該教孩子學什麼語言比較好:LOGO、VB還是炙手可熱的Python?雖然我非常熟悉C#,但需要了解許多背景知識,還需要安裝一個很大的框架環境,顯然C#不適合小學生學習,Java也是。LOGO是老牌的兒童程式語言了,操控一個小海龜來畫圖很形象,VB入門簡單,但要一個小學生熟悉它的整合開發環境要求還是高了點,選Python無非就是因為AI應用火它就火,除此之外我找不出它適合兒童使用的理由。

我覺得給小學生使用的程式語言,要足夠簡單:

1,程式設計環境足夠簡單,一個命令列就行,不需要一個強大的IDE,否則用它還得熟悉很多選單按鈕和概念;

2,語法要足夠簡單,最好連變數都不需要定義,沒有各種複雜的程式結構或語句,不需要了解方法、類、包、模組這些東西,拿來就能寫程式跑起來;

3,資料結構要簡單,什麼陣列、佇列、堆疊或者樹等等不要太多。

    想到這裡,唯一滿足要求的就是Lisp語言了,它簡單到只有3種最基本的資料結構:原子、表和字串;只有一種語法,就是符號表達式,資料和函式都是採用符號表達式定義的,這種符號表達式稱為S一表達式,它是原予和表的總稱。Lisp衍生出了很多方言,形成一個龐大的Lisp語言家族,Scheme是其中最簡單的方言,而且很長時間都是美國麻省理工學院計算機系的教學語言,Scheme的發明者和推動者都是數學家、科學家和教育學家,所以它一開始就有數學的基因,非常適合作為一種入門的計算機教學語言。前面說到孩子上數學課需要,那麼Scheme語言就是不二之選了,B站有一個小學生用Scheme語言來表達三角形定理應用的小視訊,可以點選這裡檢視。

專案程式簡介

    既然決定教孩子Scheme語言,那我就得先熟悉一下它了,之前斷斷續續學習了幾次,一直沒有真正用過它所以始終沒學好,現在疫情期間,正好可以用它來寫一個程式練手,並且使用這個有實際意義的程式作為小學生學習Scheme語言的入門教程,於是有了本篇文章標題說的這個專案,一個2019新型冠狀病毒肺炎(COVID-19)感染風險自助檢測程式。為了更好的介紹Scheme語言,本文將結合這個例項程式來介紹Scheme語言的語法元素,於是有了這篇部落格文章。讀者可以先從下面的倉庫地址克隆一份,包括原始碼和Scheme執行程式。

原始碼倉庫:https://github.com/bluedoctor/Check-COVID-19

程式名稱:2019新型冠狀病毒肺炎(COVID-19)感染風險自助檢測程式

程式功能:

根據網路上收集整理的新冠肺炎臨床症狀表現,以及國家衛生健康委員會與國家中醫藥管理局釋出了《新型冠狀病毒肺炎診療方案》等資料, 整理的新冠肺炎臨床症狀表現、醫院檢查和流行病學調查情況,設計的一個風險測試表,然後根據這個風險測試表定義的診斷知識編寫程式,互動式的引導使用者回答提問,最後給出診斷結果。 你也可以調整這裡定義的各個指標風險值,以使它更接近實際的效果。

 有了這個風險測試表,如何簡單有效的用程式表示,這也是我選擇使用Scheme語言來寫這個程式的原因,因為它的S表示式具有程式和資料的一致性,也就是說我們的知識資料可以表達為一種等價的程式結構,比如將上表的身體症狀表達為下面的程式結構:

; 2019新型冠狀病毒肺炎(COVID-19)感染風險自助檢測程式
(define A1 (list "發熱" (cons "三天內" 5) (cons "三天到一週" 10) (cons "超過一週" 15)))
(define A2 (list "咳嗽" (cons "無痰" 15) (cons "有痰難吐" 10) (cons "有痰易吐" -10)))
(define A3 (list "乏力" (cons "無" -15) (cons "輕微" 15) (cons "明顯" 30)))
(define A4 (list "腹瀉" (cons "無" 0) (cons "輕微" 10) (cons "明顯" 5)))
(define A5 (list "呼吸困難" (cons "無" 0) (cons "略感胸悶" 15) (cons "明顯" 30)))

這個症狀風險知識的程式表示程式碼涉及的Scheme語言概念下面逐一介紹。

Scheme語言基礎

1,表示式

最簡單的表示式是常量物件,如字串、數字、符號和列表。表示式支援其它物件型別,但這四種物件對大多數程式已經足夠了。

數字型(number)

它又分為四種子型別:整型(integer),有理數型(rational),實型(real),複數型(complex);它們又被統一稱為數字型別(number)。
如:複數型(complex) 可以定義為 (define c 3+2i)
實數型(real)可以定義為 (define f 22/7)
有理數型(rational)可以定義為 (define p 3.1415)
整數型(integer) 可以定義為 (define i 123)

符號型別(symbol)

是Scheme語言中有多種用途的符號名稱,它可以是單詞,用括號括起來的多個單詞,也可以是無意義的字母組合或符號組合,它在某種意義上可以理解為C中的列舉型別。可以使用quote操作符定義一個符號,也可以單引號'開頭來簡單表示一個符號,如下面的示例:

> (quote a)
a
>'a
a

在Lisp/Scheme 中,通常都需要對錶達式進行求值,而符號(通常)不對自身求值,所以要是想引用符號,應該像上例那樣用 ' 引用它。

複合表示式

由操作符、常量物件或者表示式組合而成。例如下面這個計算兩個數相加的簡單表示式:

> (+ 1 2)
3

通過這個程式示例看到,Scheme的表示式是字首表示式,也就是說把運算子放在最左側。這樣做的優點是可以定義帶任意個數的實參過程。

上面這個複合表示式如果想引用它而不是立即求值,就需要把它定義成符號:

> '(+ 1 2)
(+ 1 2)

這樣,我們可以在後續需要的時候將這個符號轉換成普通的表示式讓它求值。

S-表示式

Lisp 要求我們直接在抽象語法上工作。這個抽象的語法樹用成對的括號表示。這樣的結構在 Lisp 圈子裡面被稱為 sexp 表示式,俗稱S-表示式。S-表示式的第一個單詞決定了 sexp 的意義,剩下的單詞都是引數,記住這一條規則足夠了。這個規則使得我們不需要記憶其它程式語言那些複雜的語法結構,入門使用變得極其簡單。我們在設計程式的時候應該始終圍繞這個抽象語法進行,我們的程式設計的越抽象,那麼程式就越接近問題的本質。

Lisp 程式看成是完全由"函式呼叫"這個單一的語法結構構成。 Lisp 裡面沒有為了算術表示式、或者邏輯表示式、或者語言的關鍵字,比如 IF 和 THEN,來準備特別的語法結構。所有的語言元素在 Lisp 裡面都是按照這個簡單一致的語法結構來安排,整個程式就是一個表示式,程式的執行就是對錶達式進行求值。

2,原子

 Lisp中有一個叫原子的東西,不可再分,是一個很基礎的概念。原子可以是任何數,分數,小數,自然數,負數等等。原子可以是一個字母排列,當然其中可以夾雜數字和符號。除了表和所有函式以外均是原子。

Scheme沒有直接說原子這個概念,但Scheme作為Lisp的方言,在形式上還是有原子這樣的東西。所有的 Lisp/Scheme 表示式,要麼是 1 這樣的數原子,要麼是包在括號裡,由零個或多個表示式所構成的列表。所以可以這樣說,List程式裡面就是原子和表。這就是Lisp 表示法一個美麗的地方是:它是如此的簡單!

3,表(list)

 表是由多個相同或不同的資料連續組成的資料型別,它是程式設計中最常用的複合資料型別之一,很多過程操作都與它相關。下面是在Scheme中表的定義和相關操作:

> (define la (list 1 2 3 4 ))
>la
(1 2 3 4)
> (length la) ; 取得列表的長度
4
> (list-ref la 3) ; 取得列表第3項的值(從0開始)
4
> (list-set! la 2 99) ; 設定列表第2項的值為99
99
> la
(1 2 99 4)
> (define y (make-list 5 6)) ;建立列表
> y
(6 6 6 6 6)

在上面的例子中,使用了函式list 來構造具有4個元素的表,然後使用define函式來定義一個變數 la,將變數la與前面定義的表相繫結。除了使用list函式形勢來構造表,還可以使用引用方式來使用一個表,下面的程式碼與上面定義la 變數繫結表是等價的:

> (define la (list 1 2 3 4 ))
>la
(1 2 3 4)
> (define la '(1 2 3 4 ))
>la
(1 2 3 4)

回到文章開頭,我在這個小專案中首先就定義了幾個表並且與變數相繫結:

(define A1 (list "發熱" (cons "三天內" 5) (cons "三天到一週" 10) (cons "超過一週" 15)))

 

4,點對(pair)

List/Scheme 的基本構成元素是所謂的 Pair。什麼是 Pair 呢??我們可以按照這個英語單詞在日常生活中最常見的意思,比如一個座標點,來想象這個最基礎的資料結構。比如座標(1,2)是一個點對。一個點對包含兩個指標,每個指標指向一個值。我們用函式cons構造點對。比如說(cons 1 2)就構造出點對(1 . 2)。因為點對總是由函式cons構造,點對又叫做cons cell。點對左邊的值可以用函式car取出來,右邊的值可以由函式cdr取出來。比如列表(1 2 3 4),實際上由點對構成:(1 . (2 . (3 . 4. ‘())。可以看出,列表本質是單向連結串列。

在當前例項程式的表變數A1中,我們構造了一個具有發熱症狀對應風險屬性的表,它有三個點對元素,分別是:

( 三天內 . 5  ) ( 三天到一週 . 10  )  (超過一週 . 15)

構造這3個點對元素分別通過下面的三個表示式實現:

(cons "三天內" 5)(cons "三天到一週" 10)(cons "超過一週" 15)

在表變數A1 中,可以通過cdr函式得到這3個點對元素:

>(car A1)

發熱

>(cdr A1)

( 三天內 . 5  ) ( 三天到一週 . 10  )  (超過一週 . 15)

可以看出,我們在程式中,使用點對模擬了症狀屬性和對應的風險值結構,類似於.NET中的“名-值”對結構。所以,我們通過Scheme程式,實現了新冠病毒臨床診斷知識表達的 “症狀--屬性--風險值” 三元結構,我們的程式就是匹配患者的這些症狀屬性,從而計算出相對應的風險值。Scheme的表和點對結構,使得我們對於這類知識的表達更直觀更容易。

5,向量(vector)

向量可以說是一個非常好用的型別 ,是一種元素按整數來索引的物件,異源的資料結構,在佔用空間上比同樣元素的列表要少,在外觀上:
列表示為: (1 2 3 4)
VECTOR表示為: #(1 2 3 4)

可以通過下面的程式碼來定義和查看向量的表示:

>(define v (vector 1 2 3 4 5))
#(1 2 3 4 5)
>(define v ‘#(1 2 3 4 5))
#(1 2 3 4 5)
> (vector-ref v 0) ; 求第n個變數的值
1
> (vector-length v) ; 求vector的長度
5
> (vector-set! v 2 "abc") ; 設定vector第n個元素的值
> v
#(1 2 "abc" 4 5)
> (define x (make-vector 5 6)) ; 建立向量表
> x
#(6 6 6 6 6)

在當前專案例項中,我們將所有相關的症狀變數放到一個向量中:

(define QA (vector A1 A2 A3 A4 A5))

之後,我們會在程式中迴圈遍歷這些症狀表,獲取向量QA的長度,獲得QA指定索引的表元素。

 

6,變數

變數定義:
可以用define來定義一個變數,形式如下:
(define 變數名 值)
例如,上面定義了一個變數QA,它的值是一個向量。

前面的示例中好多地方都採用了這種方式來定義變數,但這種方式定義的是一個全域性變數,但很多時候,我們需要使用區域性變數,以消除全域性變數可能意外被修改的影響。

定義區域性變數需要使用let表示式,如下所示:

> (let ((x 1) (y 2))
     (+ x y))
3

這裡定義了兩個變數:x和y,定義的時候給它們分別繫結一個初始值1和2。

let表示式的形式定義是:

(let [binds]

  bodys

)

變數在binds定義的形式中被宣告並初始化。body由任意多個S-表示式構成。binds的格式如下:
[binds] → ((p1 v1) (p2 v2) ...)
聲明瞭變數p1、p2,並分別為它們賦初值v1、v2。變數的作用域(Scope)為body體,也就是說變數只在body中有效。body體的最後一個表示式的值為let表示式的值。

更改變數的值:
可以用set!來改變變數的值,格式如下:
(set! 變數名 值)

如在當前例項程式中:

(define Total_Risk 0) ;定義全域性 風險總值 變數
;其它程式碼略
 (display "單純性發熱,調低風險值。")(newline)
 (set! Total_Risk (- Total_Risk 15))

Scheme語言是一種高階語言,和很多高階語言(如python,perl)一樣,它的變數型別不是固定的,可以隨時改變。

7,函式

Scheme語言中函式不同於其它語言的函式,它被稱為過程(Procedure)。過程是一種資料型別,這也是為什麼Scheme語言將程式和資料作為同一物件處理的原因。

如果我們在Chez Scheme提示符下輸入加號然後回車,會出現下面的情況:

> +
#<procedure +>

這告訴我們"+"是一個過程,而且是一個原始的過程,即Scheme語言中最基礎的過程,在GUILE中內部已經實現的過程,這和型別判斷一樣,如boolean?等,它們都是Scheme語言中最基本的定義。

define不僅可以定義變數,還可以定義過程,因在Scheme語言中過程(或函式)都是一種資料型別,所以都可以通過define來定義。不同的是標準的過程定義要使用lambda這一關鍵字來標識。
我們可以自定義一個簡單的過程並使用,如下:

> (define add1 (lambda (x) (+ x 1)))
> add1
#<procedure add1>
> (add1 5)
6

在Scheme語言中,也可以不用lambda,而直接用define來定義過程,它的格式為:
(define (過程名 引數) (過程內容 …))

在當前例項程式中,都使用了這種方式來定義過程,個人覺得這種方式更方便些。例如下面定義一個過程確保使用者輸入一個數字並返回:

(define (input_selected )
    (let loop ()
        (display "請輸入你選擇的答案對應的數字:")  
        (let ((k (read)))   
            (if (integer? k)    
                k ;return
                (begin
                     (display "輸入錯誤!")  
                     (newline)
                     (loop))))))

在Scheme語言中,過程定義也可以巢狀,一般情況下,過程的內部過程定義只有在過程內部才有效,相當C語言中的區域性變數。

8,型別判斷

Scheme語言中所有判斷都是用型別名加問號再加相應的常量或變數構成,例如上面定義的函式input_selected 中使用 integer? 來判斷使用者輸入的內容是否是一個整數:(integer? k)

也可以使用null?判斷一個物件是否是空型別:

(null? (cdr name-values))

 

9,邏輯運算

邏輯運算實際上是計算一個邏輯表示式。原子邏輯表示式是布林物件,在Scheme中使用 #t 表示true,#f 表示false。

邏輯表示式支援not,and,or 邏輯操作,型別判斷的結果也是一個邏輯表示式,例如在本專案例項中:

 ;測試是否有嚴重腹瀉並且沒有咳嗽
  (let ((FU_XIE (have_attribute_inResult Ctx_Attributes "腹瀉")))
      (if (and    (not (null? FU_XIE)) 
                  (equal? "明顯" (car (cdr FU_XIE))) 
                  (null? KE_SOU)
          )
          (set! Total_Risk 0) ;為嚴重腹瀉引起的乏力,不會是新冠感染。
          (begin
              (display "乏力伴隨咳嗽或者發熱,調高風險值。")(newline)
              (set! Total_Risk (* Total_Risk 2))
          )
      )
  )

 and和or表示式都可以處理多個邏輯表示式引數,例如上面的and 表示式處理了三個邏輯判斷表示式。

10,程式碼塊(form)

 塊(form)是Scheme語言中的最小程式單元,一個Scheme語言程式是由一個或多個form構成。沒有特殊說明的情況下 form 都由小括號括起來,所以一個form最小可以是一個表示式,也可以是一個變數定義,一個過程定義。form允許巢狀,這使它可以輕鬆的實現複雜的表示式,同時也是一種非常有自己特色的表示式。有時候需要將多個表示式組合成一個form,比如在分支判斷後執行多個操作,需要在一個塊中,這個功能可以通過begin來實現,例如上面邏輯運算的例子。

11,分支結構

if結構:

Scheme語言的if結構有兩種格式:
(if (測試條件)(滿足測試條件時 要執行的過程1))
(if (測試條件)(滿足測試條件時 要執行的過程1)
                       (否則 要執行的過程2) )

要執行的過程1和2可以是一個表示式,也可以是多個表示式,如果是多個表示式,需要使用begin語句塊。if結構的示例程式碼比較多,這裡就不再重複了。

cond結構:

Scheme語言中的cond結構類似於C語言中的switch結構,cond的格式為:

(cond ((測試) 操作) … (else 操作))

cond結構的每個測試都可以是不同的邏輯表示式,相當於多個巢狀的if...else if...結構,在當前例項專案中就曾經使用cond結構來優化,示例程式碼如下:

;從結果中判斷是否有指定的症狀屬性;如果有,返回症狀特徵表;如果沒有,返回空表
(define (have_attribute_inResult result attName)
    (let loop ( (lst result) )
        (let ( (car_lst (car lst))  (cdr_lst (cdr lst)))
            ;(if (equal? attName (car car_lst))
            ;    car_lst
            ;     (if (null? cdr_lst)
            ;        '()
            ;        (loop cdr_lst)
            ;    )
            ;)
            ;
            (cond   ((equal? attName (car car_lst))  car_lst )
                    ((null? cdr_lst) '())
                    (else (loop cdr_lst))
            )
        )
    )
)

case結構:

case結構和cond結構有點類似,它的格式為:

(case (表示式) ((值) 操作)) ... (else 操作)))

case結構中的值可以是複合型別資料,如列表,向量表等,只要列表中含有表示式的這個結果,則進行相應的操作,如下面的程式碼:

 >(case (* 2 3)
   ((2 3 5 7) 'prime)
   ((1 4 6 8 9) 'composite))
composite

上面的例子返回結果是composite,因為列表(1 4 6 8 9)中含有表示式(* 2 3)的結果6;

在當前例項專案中,使用case結構來進行流行病調查分析:

(display "2)最近14天,您是否去過 重點疫區國家?(0-未去過,1-義大利、西班牙,2-歐洲其它地方,3-美國,4-世界其它地方)")
(newline)
(set! Total_Risk (+ Total_Risk 
    (case (input_selected)
        ((0) 0)
        ((1) 50)
        ((2 4) 30)
        ((3) 80)
        (else 0)
    )
))

12,迴圈結構

使用遞迴實現迴圈:

Scheme最原始的語法結構是沒有迴圈的,要實現迴圈功能,可以通過遞迴呼叫過程實現:

>(define loop
       (lambda(x y)
           (if (<= x y)
               (begin
                  (display x) (display #\\space)
                 (set! x (+ x 1))
       (loop x y)))))
>(loop 1 10)
1 2 3 4 5 6 7 8 9 10

使用遞迴過程來實現迴圈雖然比較直觀自然,然而它的效率不高。要改善遞迴效率,可以將遞迴過程修改為尾遞迴,Scheme語言規範規定尾遞迴將在語言內部優化為迴圈結構。有關尾遞迴的概念這裡不再詳細介紹。

使用命名let:

let表示式本質上是一個Scheme語法糖,它內部轉換成了lambda表示式呼叫。命名let在Scheme中與尾遞迴有相似的效果,具體可以參考這篇文章。

在本例項專案中,使用了命名let來實現迴圈,例如前面示例的函式have_attribute_inResult的定義裡面使用了命名let,下面看一個簡單的例子,重新看到input_select函式的示例,注意let後的符號 loop就是它的名字:

(define (input_selected )
    (let loop ()
        (display "請輸入你選擇的答案對應的數字:")  
        (let ((k (read)))   
            (if (integer? k)    
                k ;return
                (begin
                     (display "輸入錯誤!")  
                     (newline)
                     (loop))))))

在上面的過程中,如果輸入的不是整數,會要求重新輸入,迴圈執行這個過程直到輸入正確為止,在最後一行通過重新呼叫前面名字為 loop的let實現迴圈效果。而在函式have_attribute_inResult中,命名let開始的時候將變數lst的初始值繫結為函式引數result,而在方法後面部分呼叫名字loop的let時候,使用變數cdr_lst來更新最這個命名let的值:

(define (have_attribute_inResult result attName)
    (let loop ( (lst result) )
        (let ( (car_lst (car lst))  (cdr_lst (cdr lst)))
             (cond   ((equal? attName (car car_lst))  car_lst )
                    ((null? cdr_lst) '())
                    (else (loop cdr_lst))
            )
        )
    )
)

使用do迴圈表示式:

就像在C系列語言中我們通常用while比較多而do比較少一樣,在scheme中do也並不常見,但語法do也可以用於表達重複。它的格式如下:

    (do binds
       (predicate value)
        body)

變數在binds部分被繫結,而如果predicate被求值為真,則函式從迴圈中逃逸(escape)出來,並返回值value,否則迴圈繼續進行。binds部分的格式如下所示:
[binds] - ((p1 i1 u1) (p2 i2u2)...)
變數p1,p2,…被分別初始化為i1,i2,…並在迴圈後分別被更新為u1,u2,…。

在當前例項專案中,也使用了do迴圈表示式,如下所示:

 1 ;獲取症狀特徵列表指定的特徵序號(從1開始的序號)對應的風險值
 2 (define (get-index-value lst_attribute index)
 3     (do ((name-values (cdr lst_attribute) (cdr name-values)) 
 4          (i 1 (+ i 1)))
 5         ( (or (= i index) (null? (cdr name-values))) ;break
 6           (cdr (car name-values)) ;return value
 7         )
 8         ;(display (cdr name-values ))   
 9         ;(newline) 
10     )
11 )

在上面的第3行程式碼中,迴圈變數name-values繫結的初始值是表示式(cdr lst_attribute),而在後續的迴圈過程中,迴圈變數name-values將被更新為一個新的值:表示式(cdr name-values)的值,也就是後續的症狀(屬性.風險值)點對。第5行是跳出迴圈的條件表示式,第6行程式碼是do迴圈表示式在終止迴圈後返回的值。do迴圈的body程式碼部分可以沒有,上面的示例中將body程式碼註釋了。

系統設計方案

1,診斷方案

根據《新型冠狀病毒肺炎診療方案》,這裡整理一個簡單的診斷方案。這裡需要從三方面進行。

首先是對患者身體症狀的問診,詢問是否有發熱、咳嗽、腹瀉、乏力、呼吸困難等新冠肺炎典型的症狀,再根據具體的症狀表現來分析感染病毒的風險機率,經過綜合分析,設計出每一個具體症狀表現的風險值。

然後是醫院化驗檢測報告分析,主要有胸部CT、核酸檢測以及全血化驗中的白細胞計數情況,如果核酸檢測陽性那感染新冠病毒的機率就是100%,但檢測陰性並不代表沒有感染機率,核酸檢測有一個準確性問題以及隨著病情發展的檢測時間問題,都會影響檢測結果準確性。所以後來對很多疑似患者採取了依據肺部CT結果的臨床確診。最後就是白細胞計數,病毒感染一般白細胞計數都不會有明顯變化,甚至下降,如果白細胞計數顯著增高,那麼要排除病毒感染,但也可能是合併細菌性炎診感染。多以對於醫院化驗檢測情況也需要綜合設定一個風險值。

第三就是流行病學調查,對於有疑似感染的情況,如果與確診患者有接觸,或者去過重點疫區,那麼感染的風險就很高,反之就較低。根據流調的具體情況,對每一類情況設定一個相應的風險值。

綜合這三方面的分析,制定一個風險測試表。不過這個表也僅僅只能做一個參考,一方面具體的風險值需要專家來綜合設定,另一方面這些風險因素是相互影響的,它們的關係不是一個表格這麼簡單,所以需要把這個風險測試表再根據患者有的測試專案進行綜合分析,根據一些診斷規則,對風險值進行修正。所以這個診斷方案,非常依賴行業專家的知識,包括科學家、醫學家以及臨床一線醫生。鑑於筆者能力水平,當前這個診斷方案肯定會有很多問題,僅供參考和學習Scheme語言程式設計使用。

2,專家系統

專家系統是一個具有大量的專門知識與經驗的程式系統,是一種模擬人類專家解決領域問題的計算機程式系統。顯然,寫一個診斷新冠病毒感染風險的程式可以是一個專家系統,前面也說了這個診斷方案是依賴於專家的知識經驗的。
專家系統通常由人機互動介面、知識庫、推理機、直譯器、綜合資料庫、知識獲取等6個部分構成。其中尤以知識庫與推理機相互分離而別具特色。下圖是專家系統的一個結構示意圖:

當前這個“診斷專家系統”程式中,也具有專家系統基本的元素,它們的設計方案是:

  • 人機介面:採用互動式命令列方式,系統提問,使用者根據提是選擇回答問題,引導使用者完成所有問題的測試。
  • 知識庫:將“新冠病毒感染風險測試表”的知識使用Scheme程式表示。
  • 全域性資料庫:一個儲存當前患者物件診斷資料的“特徵上下文”物件,它是一個列表。
  • 解釋機制:每當使用者回答一個問題,都會給出當前特徵項的風險值,並在診斷完成後給出診斷資料。
  • 推理機:它包括2個工作,一是根據使用者回答的問題計算感染風險的累加值,另一個是根據一些特殊的風險屬性相關性規則,來修正風險值,比如對於乏力症狀特徵與其它症狀特徵之間的關係進行處理的規則。

3,推理機制

採用不確定性推理機制。不確定性推理是指那種建立在不確定性知識和證據的基礎上的推理。它實際上是一種從不確定的初始證據出發,通過運用不確定性知識,最終推出既保持一定程度的不確定性,又是合理和基本合理的結論的推理過程,目的是使計算機對人類思維的模擬更接近於人類的真實思維過程。

在當前專案中,患者的症狀表現就是疾病診斷的一種證據,但這種證據所代表的診斷知識是不確定的。比如發熱症狀,很多疾病都會引起發熱,一般感冒和流感都會發熱,但發熱的症狀表現(例如持續時間和體溫)是不同的,同一個症狀表現也可能是不同疾病引起的,所以針對某一種疾病而言,每個症狀表現都只能是一個該種疾病的確診概率,需要綜合多個症狀表現才能在一個大概率上確認或者排除一種疾病。比如發熱三天以內,沒有咳嗽和呼吸困難,也沒有乏力表現,那麼是新冠的可能性就很低,如果還有鼻塞流鼻涕的表現,那麼普通感冒的可能性就很大了。

在當前專案中,不確定性知識的表達採用【物件】-【特徵】-【值】結構,【值】是一個“點對”結構,這個“點對”結構表示【值】的名稱和【值】的數值結果,在當前程式中,【值】的具體數值表示感染風險值。例如發熱是患者物件的一個症狀特徵,發熱的具體表現,也就是特徵的值,例如發熱”三天內“是發熱症狀的特徵值,它對應的新冠感染風險是5%。在當前專案中,發熱症狀的完整特徵值表達為Scheme語言的“列表”,如下所示:

(list "發熱" (cons "三天內" 5) (cons "三天到一週" 10) (cons "超過一週" 15))

為了便於引用這個發熱症狀的不確定性知識,我們將它繫結到一個變數上,相當於為它定義一個編號:

(define A1 (list "發熱" (cons "三天內" 5) (cons "三天到一週" 10) (cons "超過一週" 15)))

這樣,將咳嗽、乏力、腹瀉、呼吸困難等症狀都定義一個編號,組成一個身體症狀“矩陣”,這個矩陣結構用一個向量來表示:

;定義 患者的身體症狀特徵
(define A1 (list "發熱" (cons "三天內" 5) (cons "三天到一週" 10) (cons "超過一週" 15)))
(define A2 (list "咳嗽" (cons "無痰" 15) (cons "有痰難吐" 10) (cons "有痰易吐" -10)))
(define A3 (list "乏力" (cons "無" -15) (cons "輕微" 15) (cons "明顯" 30)))
(define A4 (list "腹瀉" (cons "無" 0) (cons "輕微" 10) (cons "明顯" 5)))
(define A5 (list "呼吸困難" (cons "無" 0) (cons "略感胸悶" 15) (cons "明顯" 30)))
(define QA (vector A1 A2 A3 A4 A5))

使用【物件】-【特徵】-【值】結構,我們處理可以定義患者的身體症狀特徵,還可以定義患者的醫院化驗檢查結果特徵:

;定義患者的醫院化驗檢查結果特徵
(define B1 (list "胸部CT" (cons "正常/未檢測" 1) (cons "肺部毛玻璃樣" 80) (cons "其它情況" 20)))
(define B2 (list "病毒核酸檢測" (cons "未檢測" 5) (cons "陽性" 100) (cons "陰性" 20)))
(define B3 (list "白細胞計數" (cons "正常/未檢測" 1) (cons "偏低" 20) (cons "增高" -20)))
(define QB (vector B1 B2 B3 ))

現在可以看到,通過這個【物件】-【特徵】-【值】結構,我們可以表示一般的“不確定性知識”了,甚至用來表示學生考試題的答題選擇率,用來發現班級的教學問題。

上面的每個特徵都將形成一個要向患者詢問的問題,這些問題的回答結果將在患者物件身上形成一個特徵上下文。特徵上下文的結構是一個表,包含多個具體的特徵表,例如:
( (特徵名1 特徵值名 特質值) (特徵名2 特徵值名 特質值) ...)

在當前程式中,它的值可能像下面這個樣子:

 ((發熱 三天內 5) (咳嗽 無痰 15) (乏力 無 -15) (腹瀉 無 0) (呼吸困難 無 0))

患者物件的特徵上下文物件,在程式中通過下面一行程式碼實現:

(define Ctx_Attributes '())

4,推理機的實現

有了推理所需要的不確定性知識表達結構,那麼進行推理就很方便了:特徵匹配。推理過程就是在與使用者的互動過程中,通過詢問使用者的問題,如果該問題與預先定義的不確定性特徵知識相匹配,那麼就可以計算它對應的概率值(在本專案中是風險值)。推理的結果存放到特徵上下文物件中,在本程式中,它是患者物件的症狀問題上下文。

在本程式中,推理機的實現就是過程process-question 的定義,它會遍歷特徵向量中的每一個特徵表,計算出匹配的特徵值。過程實現的詳細程式碼如下:

(define (process-question listAttributes)
    (let loop ((i 0) (j (- (vector-length listAttributes) 1)))
    (display (+ i 1))
    (display ",您最近是否有【")
    (let ((Ai (vector-ref listAttributes i)))
        (display (car Ai))
        (display "】的情況?(如果有,請輸入數字1;否則輸入其它字元以跳過此項檢測。)")
        (newline)
        ;(let ((input 1)) ;test
        (let ((input (read)) (Ai_Name (car Ai)))
            (if (and (integer? input) (= input 1))
                (begin
                    (display Ai_Name)
                    (display "的具體情況是:")
                    (show-attribute Ai)
                    (newline)
                    (let ((q_index (input_selected )))
                        (display "您當前選擇的情況風險值是:")
                        (let ((curr_risk (get-index-value Ai q_index))) 
                            (display curr_risk)
                            (set! Ctx_Attributes (append Ctx_Attributes 
                                (list (list Ai_Name (get-index-name Ai q_index) curr_risk ))))
                            (set! Total_Risk (+ Total_Risk curr_risk)) 
                        )
                        
                    ) 
                )
                (begin
                    (display "您沒有【")
                    (display Ai_Name)
                    (display "】的情況。")
                )
            )
        )
        (newline)
        (newline)
    )

    (if (< i j)
        (loop (+ i 1) j))
))

下面我們就可以呼叫這個“推理機”來處理問題了:

(display " 一、開始身體症狀測試 :")(newline)
(process-question QA)
(display "初步診斷詳細內容:")
(display Ctx_Attributes )
(newline)  
;其它程式碼略
(display " 二、開始進行【醫院檢測結果】分析 :")(newline)
(process-question QB)

 

結語

當前專案例項程式的完整程式碼請去前面說的Git倉庫克隆一份,整個程式碼不到300行,其中很多程式碼已經作為Scheme語言基礎的示例來介紹了,所以這裡不再重複。通過這個專案,我們看到用Scheme語言來實現這樣一個新冠病毒感染風險測試的專家系統是比較簡單的,相信你閱讀了本篇問診並且下載執行這個程式之後,已經踏入了Scheme語言學習的大門。

&n