Faster RCNN程式碼詳解(四):關於anchor的前世今生
在上一篇部落格中介紹了資料處理的整體結構:Faster RCNN程式碼詳解(三):資料處理的整體結構。這一篇部落格介紹資料處理的細節——關於anchor的前世今生,程式碼在指令碼的:~/mx-rcnn/rcnn/io/rpn.py的assign_anchor函式中。
這一部分也是你想要深入瞭解Faster RCNN演算法細節的重要部分,因為anchor是Faster RCNN演算法的核心之一。具體而言,在這篇部落格中我將為你介紹:anchor是什麼?怎麼生成的?anchor的標籤是怎麼定義的?bbox(bounding box)的迴歸目標是怎麼定義的?bbox和anchor是什麼區別?
def assign_anchor (feat_shape, gt_boxes, im_info, feat_stride=16,
scales=(8, 16, 32), ratios=(0.5, 1, 2), allowed_border=0):
"""
assign ground truth boxes to anchor positions
:param feat_shape: infer output shape
:param gt_boxes: assign ground truth
:param im_info: filter out anchors overlapped with edges
:param feat_stride: anchor position step
:param scales: used to generate anchors, affects num_anchors (per location)
:param ratios: aspect ratios of generated anchors
:param allowed_border: filter out anchors with edge overlap > allowed_border
:return: dict of label
'label': of shape (batch_size, 1) <- (batch_size, num_anchors, feat_height, feat_width)
'bbox_target': of shape (batch_size, num_anchors * 4, feat_height, feat_width)
'bbox_inside_weight': *todo* mark the assigned anchors
'bbox_outside_weight': used to normalize the bbox_loss, all weights sums to RPN_POSITIVE_WEIGHT
"""
def _unmap(data, count, inds, fill=0):
"""" unmap a subset inds of data into original data of size count """
if len(data.shape) == 1:
ret = np.empty((count,), dtype=np.float32)
ret.fill(fill)
ret[inds] = data
else:
ret = np.empty((count,) + data.shape[1 :], dtype=np.float32)
ret.fill(fill)
ret[inds, :] = data
return ret
im_info = im_info[0]
scales = np.array(scales, dtype=np.float32)
# base_anchors是anchor的初始化結果,輸入中base_size=16,表示輸入影象到該層
# feature map的尺寸縮小倍數,對於resnet網路的conv4_x而言縮小倍數是16;ratios預設是[0.5,1,2];
# scales預設是[8,16,32]。base_anchors預設是9*4的numpy array,表示9個anchor的4個座標值,
# 4個座標值用框的左上角座標和右下角座標。這9個anchor有一個共同點是中心座標點一樣,
# 這正是和RPN網路的滑窗操作對應(第一個3*3的卷積層),滑窗每滑到一個3*3區域,
# 則以該區域中心點為座標就會生成9個anchor。
base_anchors = generate_anchors(base_size=feat_stride, ratios=list(ratios), scales=scales)
num_anchors = base_anchors.shape[0]
# feat_height和feat_width表示該層feature map的size,比如對於resnet的res4而言,
# 縮放係數是16,所以如果輸入影象是600*900,則feat_height=600/16,feat_width=900/16
feat_height, feat_width = feat_shape[-2:]
logger.debug('anchors: %s' % base_anchors)
logger.debug('anchor shapes: %s' % np.hstack((base_anchors[:, 2::4] - base_anchors[:, 0::4],
base_anchors[:, 3::4] - base_anchors[:, 1::4])))
logger.debug('im_info %s' % im_info)
logger.debug('height %d width %d' % (feat_height, feat_width))
logger.debug('gt_boxes shape %s' % np.array(gt_boxes.shape))
logger.debug('gt_boxes %s' % gt_boxes)
# 1. generate proposals from bbox deltas and shifted anchors
# 前面說到base_anchors是滑窗每滑到一個區域時生成的9個anchor,因為滑窗所作用的物件
# 是38*50的feature map,當stride=1時,一共要滑動38*50次,也就是一共會得到
# 38*50*9=17100個anchor,接下來這部分程式碼就是要完成這樣的操作。
# shift_x和shift_y是根據feat_stride生成的偏移量,
# 根據這兩個偏移量就可以將前面計算的base_anchors推廣到38*50中的每個點。
shift_x = np.arange(0, feat_width) * feat_stride
shift_y = np.arange(0, feat_height) * feat_stride
shift_x, shift_y = np.meshgrid(shift_x, shift_y)
shifts = np.vstack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel())).transpose()
# add A anchors (1, A, 4) to
# cell K shifts (K, 1, 4) to get
# shift anchors (K, A, 4)
# reshape to (K*A, 4) shifted anchors
# A預設是9
A = num_anchors
# K其實就是該層feature map的寬*高,比如高是38,寬是50,那麼K就是1900。
# 注意all_anchors最後一個維度是4,表示4個座標相關的資訊。
K = shifts.shape[0]
all_anchors = base_anchors.reshape((1, A, 4)) + shifts.reshape((1, K, 4)).transpose((1, 0, 2))
all_anchors = all_anchors.reshape((K * A, 4))
# 以前面的K是1900為例,總共會產生9*1900=17100個anchor。
total_anchors = int(K * A)
# only keep anchors inside the image
# inds_inside表示anchor的4個點座標都在影象內部的anchor的index。
inds_inside = np.where((all_anchors[:, 0] >= -allowed_border) &
(all_anchors[:, 1] >= -allowed_border) &
(all_anchors[:, 2] < im_info[1] + allowed_border) &
(all_anchors[:, 3] < im_info[0] + allowed_border))[0]
logger.debug('total_anchors %d' % total_anchors)
logger.debug('inds_inside %d' % len(inds_inside))
# keep only inside anchors
# 將不完全在影象內部(初始化的anchor的4個座標點超出影象邊界)的anchor都過濾掉,
# 一般過濾後只會有原來1/3左右的anchor。如果不將這部分anchor過濾,則會使訓練過程難以收斂。
anchors = all_anchors[inds_inside, :]
logger.debug('anchors shape %s' % np.array(anchors.shape))
# label: 1 is positive, 0 is negative, -1 is dont care
# 前面得到的只是anchor的4個座標資訊,接下來就要為每個anchor分配標籤了,
# 初始化的時候標籤都用-1來填充,-1表示無效,這類標籤的資料不會對梯度更新起到幫助。
labels = np.empty((len(inds_inside),), dtype=np.float32)
labels.fill(-1)
# 這個條件語句是判斷輸入影象中是否包含object,標籤分配只針對ground truth中有object的影象。
# bbox_overlaps函式計算的是兩個框之間的IOU,這裡是計算每個anchor和每個object的IOU,
# 生成的overlaps(二維的numpy array,假設為n*m,n表示anchor數量,m表示object數量)的
# 每一行表示anchor,每一列表示object。 argmax_overlaps是計算每個anchor和哪個object的IOU最大,
# 維度是n*1,值是object的index。max_overlaps是具體的IOU值。
# gt_argmax_overlaps = overlaps.argmax(axis=0)則是計算每個object和哪個anchor的IOU最大,
# 維度是m*1,值是anchor的index,另外因為如果有多個anchor和某個object的IOU值都是最大且一樣,
# 那麼gt_argmax_overlaps只會得到index最小的那個,
# 所以需要gt_argmax_overlaps = np.where(overlaps == gt_max_overlaps)[0]將IOU最大的那些anchor都撈出來。
# gt_max_overlaps是具體的IOU值。
if gt_boxes.size > 0:
# overlap between the anchors and the gt boxes
# overlaps (ex, gt)
overlaps = bbox_overlaps(anchors.astype(np.float), gt_boxes.astype(np.float))
argmax_overlaps = overlaps.argmax(axis=1)
max_overlaps = overlaps[np.arange(len(inds_inside)), argmax_overlaps]
gt_argmax_overlaps = overlaps.argmax(axis=0)
gt_max_overlaps = overlaps[gt_argmax_overlaps, np.arange(overlaps.shape[1])]
gt_argmax_overlaps = np.where(overlaps == gt_max_overlaps)[0]
# 這個條件語句預設是執行的,目的是將IOU小於某個閾值的anchor的標籤都標為0,也就是背景類。
# 閾值config.TRAIN.RPN_NEGATIVE_OVERLAP預設是0.3。
# 如果某個anchor和所有object的IOU的最大值比這個閾值小,那麼就是背景。
if not config.TRAIN.RPN_CLOBBER_POSITIVES:
# assign bg labels first so that positive labels can clobber them
labels[max_overlaps < config.TRAIN.RPN_NEGATIVE_OVERLAP] = 0
# fg label: for each gt, anchor with highest overlap
# 有兩種型別的anhor其標籤是1,標籤1表示foreground,也就是包含object。
# 第一種是和任意一個object有最大IOU的anchor,也就是前面得到的gt_argmax_overlaps。
labels[gt_argmax_overlaps] = 1
# fg label: above threshold IoU
# 第二種是和所有object的IOU的最大值超過某個閾值的anchor,
# 其中閾值config.TRAIN.RPN_POSITIVE_OVERLAP預設是0.7。
labels[max_overlaps >= config.TRAIN.RPN_POSITIVE_OVERLAP] = 1
# 這一部分是和前面if not config.TRAIN.RPN_CLOBBER_POSITIVES條件語句互斥的,
# 區別在於背景類anchor的標籤定義先後順序不同,這主要涉及到標籤1和標籤0之間的覆蓋。
if config.TRAIN.RPN_CLOBBER_POSITIVES:
# assign bg labels last so that negative labels can clobber positives
labels[max_overlaps < config.TRAIN.RPN_NEGATIVE_OVERLAP] = 0
else:
# 如果ground truth中沒有object,則所有標籤都是背景。
labels[:] = 0
# subsample positive labels if we have too many
# 在RPN網路中,對回傳損失的正負樣本數量做了限定,如果不做限定的話,負樣本的數量會非常多。
# 論文中預設正負樣本的總數量是256(config.TRAIN.RPN_BATCH_SIZE),
# 因此接下來會分別針對正負樣本的數量做欠取樣,取樣通過將不需要的樣本標籤設定為-1來實現。
# 首先是對正樣本(標籤是1)的欠取樣操作(一般而言正樣本都很少,所以很少會執行欠取樣操作這個條件語句)。
# config.TRAIN.RPN_FG_FRACTION表示RPN網路最終生成的正樣本佔所有樣本的最大比例,預設是0.5,
# 表示RPN網路最終輸出的正負樣本比例一樣。num_fg是期望得到的1標籤數量,
# fg_inds是實際的1標籤數量,因此如果你實際的1標籤數量大於期望得到的1標籤數量,那麼就要做欠取樣。
num_fg = int(config.TRAIN.RPN_FG_FRACTION * config.TRAIN.RPN_BATCH_SIZE)
fg_inds = np.where(labels == 1)[0]
if len(fg_inds) > num_fg:
disable_inds = npr.choice(fg_inds, size=(len(fg_inds) - num_fg), replace=False)
if logger.level == logging.INFO:
disable_inds = fg_inds[:(len(fg_inds) - num_fg)]
labels[disable_inds] = -1
# subsample negative labels if we have too many
# 接下來這一段程式碼是對負樣本(標籤是0)的欠取樣操作(一般而言負樣本都很多,所以一般都會執行欠取樣操作)。
# num_bg就是RPN網路中batch size減去前面得到的正樣本數量。npr.choice是呼叫python中的random庫的choice函式,
# 用來從指定序列(bg_inds)中隨機選擇指定數量(size)的值。
# 預設 if logger.level == logging.INFO條件語句是執行的,這樣的話disable的樣本
# 就是從bg_inds開頭到指定數量位置,而不是隨機disable。不要的樣本標籤也是置為-1,表示無效。
num_bg = config.TRAIN.RPN_BATCH_SIZE - np.sum(labels == 1)
bg_inds = np.where(labels == 0)[0]
if len(bg_inds) > num_bg:
disable_inds = npr.choice(bg_inds, size=(len(bg_inds) - num_bg), replace=False)
if logger.level == logging.INFO:
disable_inds = bg_inds[:(len(bg_inds) - num_bg)]
labels[disable_inds] = -1
# bbox_target是每個bbox迴歸的ground truth,初始化為len(inds_inside)*4大小的numpy array,
# 所以包含了標籤為1,0和-1三種類型的bbox。bbox_transform函式用來生成bbox_targets,
# 輸入中gt_boxes原本是k*5的numpy array,k表示有幾個object,
# 這裡通過gt_boxes[argmax_overlaps, :4]擴增並取前4列值,
# 因為argmax_overlaps是和anchor的IOU最大的object的index,所以這種寫法相當於複製
# 指定index的object的gt_boxes資訊,因此某個anchor的座標迴歸目標利用的就是和
# 該anchor的IOU最大的object的座標通過一定公式轉換後的資訊,
# 這裡的公式就是bbox_transform函式實現的,或者看最後附錄截圖的後兩行公式。
bbox_targets = np.zeros((len(inds_inside), 4), dtype=np.float32)
if gt_boxes.size > 0:
bbox_targets[:] = bbox_transform(anchors, gt_boxes[argmax_overlaps, :4])
# bbox_weights
# bbox_weights變數是後續用來指定哪些anchor用於梯度更新的0,1矩陣,相當於一個mask,
# 只有標籤是1的bbox的weight才有值,值是config.TRAIN.RPN_BBOX_WEIGHTS,
# 該變數預設4個值都是1。因此標籤是0或-1的weight都是0。
bbox_weights = np.zeros((len(inds_inside), 4), dtype=np.float32)
bbox_weights[labels == 1, :] = np.array(config.TRAIN.RPN_BBOX_WEIGHTS)
if logger.level == logging.DEBUG:
_sums = bbox_targets[labels == 1, :].sum(axis=0)
_squared_sums = (bbox_targets[labels == 1, :] ** 2).sum(axis=0)
_counts = np.sum(labels == 1)
means = _sums / (_counts + 1e-14)
stds = np.sqrt(_squared_sums / _counts - means ** 2)
logger.debug('means %s' % means)
logger.debug('stdevs %s' % stds)
# map up to original set of anchors
# 前面介紹的是labels、bbox_targets和bbox_weights這3個重要變數的構建,
# 其構建的基礎都是基於indx_inside這個重要的變數,也就是將初始anchor中座標值在影象尺寸以外的bbox都過濾掉。
# 而接下來的3行程式碼則是將labels、bbox_targets和bbox_weights這3個變數重新映射回過濾之前的bbox。
# 所以假設RPN網路的輸入feature map大小是38*50,那麼最終這3個變數的第一個維度就是9*38*50=17100,
# 也就是最原始的anchor數量。當然,被過濾掉的bbox的label都是-1,bbox_targets都是0,
# bbox_weights都是0,因此對於RPN網路的訓練而言沒有幫助。
labels = _unmap(labels, total_anchors, inds_inside, fill=-1)
bbox_targets = _unmap(bbox_targets, total_anchors, inds_inside, fill=0)
bbox_weights = _unmap(bbox_weights, total_anchors, inds_inside, fill=0)
if logger.level == logging.DEBUG:
if gt_boxes.size > 0:
logger.debug('rpn: max max_overlaps %f' % np.max(max_overlaps))
logger.debug('rpn: num_positives %f' % np.sum(labels == 1))
logger.debug('rpn: num_negatives %f' % np.sum(labels == 0))
_fg_sum = np.sum(labels == 1)
_bg_sum = np.sum(labels == 0)
_count = 1
logger.debug('rpn: num_positive avg %f' % (_fg_sum / _count))
logger.debug('rpn: num_negative avg %f' % (_bg_sum / _count))
# 最後就是一些reshape操作和字典的構造並返回。最後幾個輸出的情況:labels的維度是1*17100,
# bbox_targets的維度是1*36*38*50,bbox_targets的維度是1*36*38*50。
labels = labels.reshape((1, feat_height, feat_width, A)).transpose(0, 3, 1, 2)
labels = labels.reshape((1, A * feat_height * feat_width))
bbox_targets = bbox_targets.reshape((1, feat_height, feat_width, A * 4)).transpose(0, 3, 1, 2)
bbox_weights = bbox_weights.reshape((1, feat_height, feat_width, A * 4)).transpose((0, 3, 1, 2))
label = {'label': labels,
'bbox_target': bbox_targets,
'bbox_weight': bbox_weights}
return label
介紹完了anchor的定義、生成、標籤分配,相信你對anchor的瞭解會更近一步。anchor是從初始化開始就固定了,所以anchor這個名字真的非常形象(翻譯過來是錨)。
在Faster RCNN演算法中你肯定還會經常聽到另一個名詞:region proposal(或者簡稱proposal,或者簡稱ROI),可以說RPN網路的目的就是為了得到proposal,這些proposal是對ground truth更好的刻畫(和anchor相比,座標更貼近ground truth,畢竟anchor的座標都是批量地按照scale和aspect ratio複製的)。如果你還記得在系列二中關於網路結構的介紹,那麼你就應該瞭解到RPN網路的迴歸支路輸出的值(offset)作為smooth l1損失函式的輸入之一時,其含義就是使得proposal和anchor之間的offset(RPN網路的迴歸支路輸出)儘可能與ground truth和anchor之間的offset(RPN網路的迴歸支路的迴歸目標,也就是這篇部落格程式碼中的’bbox_target’)接近。
至此,關於RPN網路中anchor的內容就都介紹完了。我們知道在Faster RCNN演算法中,RPN網路只是其中的一部分,在RPN網路得到proposal後還會經過一系列的過濾操作才會得到送入檢測網路的proposal,這個在系列二中關於網路結構的構造中已經介紹得很清楚了。但是在系列二中有一個自定一個網路層用來將2000個proposal過濾成128個,且為這128個proposal分配標籤、迴歸目標、定義正負樣本的1:3比例等,這部分算是RPN網路和檢測網路(Fast RCNN)的銜接,因此下一篇部落格就來介紹該自定義層的內容:Faster RCNN程式碼詳解(五):關於檢測網路(Fast RCNN)的proposal。
附:
Faster RCNN論文中的公式。bbox_transform函式中的第一個輸入anchors相當於這裡的xa,第二個輸入gt_boxes[argmax_overlaps, :4]相當於這裡的x*(y,w,h同理),而bbox_transform函式實現的就是截圖中下面兩行的4個式子,得到的tx*,ty*,tw*,th*就對應bbox_transform函式的輸出bbox_targets。而前面兩行式子計算的是在你得到預測的bbox資訊(x,y,w,h)後與anchor box資訊計算得到的tx,ty,tw,th。模型的迴歸部分損失函式計算是基於tx,ty,tw,th和tx*,ty*,tw*,th*。
相關推薦
Faster RCNN程式碼詳解(四):關於anchor的前世今生
在上一篇部落格中介紹了資料處理的整體結構:Faster RCNN程式碼詳解(三):資料處理的整體結構。這一篇部落格介紹資料處理的細節——關於anchor的前世今生,程式碼在指令碼的:~/mx-rcnn/rcnn/io/rpn.py的assign_anchor函式
Faster RCNN程式碼詳解(三):資料處理的整體結構
在上一篇部落格中介紹了Faster RCNN網路結構的構建:Faster RCNN程式碼詳解(二):網路結構構建。網路結構是Faster RCNN演算法中最重要兩部分之一,這篇部落格將介紹非常重要的另一部分:資料處理。 資料處理是通過AnchorLoader類
elastic-job詳解(四):失效轉移
shard out utm monit 設置 borde 點滴 title 等於 elastic-job中最關鍵的特性之一就是失效轉移。配置了失效轉移之後,如果在任務執行過程中有一個執行實例掛了,那麽之前被分配到這個實例的任務(或者分片)會在下次任務執行之前被重新分配到其他
Zookeeper詳解(四):Zookeeper中的zkCli.sh客戶端使用
zkCli.sh zookeeper客戶端 最好配置上環境變量連接操作:zkCli.sh -timeout 1000 -r -server 127.0.0.1 # -timeout 設置客戶端和服務器之間的超時時長,單位毫秒 # -r 只讀模式,不加就是讀寫模式 # -server IP:PORT 要
安卓專案實戰之強大的網路請求框架okGo使用詳解(四):Cookie的管理
Cookie概念相關 具體來說cookie機制採用的是在客戶端保持狀態的方案,而session機制採用的是在伺服器端保持狀態的方案。同時我們也看到,由於採用伺服器端保持狀態的方案在客戶端也需要儲存一個標識,所以session機制是需要藉助於cookie機制來達到儲存標識的目的,所謂ses
Tkinter 元件詳解(四):Radiobutton
Tkinter 元件詳解之Radiobutton Radiobutton(單選按鈕)元件用於實現多選一的問題。Radiobutton 元件可以包含文字或影象,每一個按鈕都可以與一個 Python 的函式或方法與之相關聯,當按鈕被按下時,對應的函式或方法將被自動執行。 Radiobutto
HTTP詳解(四):JAVA實現HTTP請求
通過上幾篇的文章,我們對HTTP已經已經有了一個初步的認識,對於"為什麼要用HTTP","怎麼用HTTP","HTTP是什麼"相信大家都有了一個了一個屬於自己的看法,今天這篇文章主要是程式碼的角度上去實現HTTP的請求。
Pygame詳解(四):event 模組
pygame.event 用於處理事件與事件佇列的 Pygame 模組。 函式 pygame.event.pump() — 讓 Pygame 內部自動處理事件 pygame.event.get() —&nb
郵件實現詳解(四)------JavaMail 發送(帶圖片和附件)和接收郵件
發送 網絡圖 發送對象 true n) com 訪問權限 sub map 好了,進入這個系列教程最主要的步驟了,前面郵件的理論知識我們都了解了,那麽這篇博客我們將用代碼完成郵件的發送。這在實際項目中應用的非常廣泛,比如註冊需要發送郵件進行賬號激活,再比如OA項目中利用郵
Quartz學習——SSMM(Spring+SpringMVC+Mybatis+Mysql)和Quartz集成詳解(四)
webapp cron表達式 msi 接口 cli post 定時 報錯 gets Quartz學習——SSMM(Spring+SpringMVC+Mybatis+Mysql)和Quartz集成詳解(四) 當任何時候覺你得難受了,其實你的大腦是在進化,當任何時候你覺得
07-Linux中DNS詳解(四)
用戶 mail all 驗證 src 更改 條目 http nslookup 接“06-Linux中DNS詳解(三)” 九、配置主從DNS服務器實現域名解析容錯 1、實驗環境zhangyujia.com(192.168.80.100)為主區域,com(192.168.8
編碼原理詳解(四)---之字形掃描
便是 集中 img 詳解 工作 -- 漢字 如何 編碼原理 上一篇我們講到,經過量化後得到了諸多零值和整數值,本篇接下來講講編碼過程中過對這些值如何組織和處理,那就是ZigZag掃描嘍。 一、簡介 ZigZag掃描也稱作之字形掃描,何以得此稱謂,是因為其掃描的路徑特
Nginx詳解(四)模塊
nginx https fastcgi 一、Nginx之目錄瀏覽二、Nginx之log模塊三、Ning之gzip模塊四、Nginx之https服務五、Nginx之fastCGI模塊 一、配置Nginx提供目錄瀏覽功能 1.修改nginx配置文件 server { listen
Keepalived詳解(四)
mysql pan 節點 ios all -s 關閉 定義 interval 一.通過vrrp_script實現對集群資源的監控: Keepalived基礎HA功能時用到了vrrp_script這個模塊,此模塊專門用於對集群中服務資源進行監控。與此模塊一起使用
PE文件格式詳解(四)
ebs 位置 數位 地址 inf font pe文件 。。 地址轉換 PE文件格式詳解(四) 0x00 前言 上一篇介紹了區塊表的信息,以及如何在hexwrokshop找到區塊表。接下來,我們繼續深入了解區塊,並且學會文件偏移和虛擬地址轉換的知識。 0x01 區塊對齊值
PE檔案格式詳解(四)
PE檔案格式詳解(四) 0x00 前言 上一篇介紹了區塊表的資訊,以及如何在hexwrokshop找到區塊表。接下來,我們繼續深入瞭解區塊,並且學會檔案偏移和虛擬地址轉換的知識。 0x01 區塊對齊值 首先我們要知道啥事區塊對齊?為啥要區塊對齊?這個問題
【SpringBoot學習之路】08.Springboot配置檔案詳解(四)
轉載宣告:商業轉載請聯絡作者獲得授權,非商業轉載請註明出處.原文來自 © 呆萌鍾【SpringBoot學習之路】08.Springboot配置檔案詳解(四) 自動配置原理 配置檔案到底能寫什麼?怎麼寫?自動配置原理; 配置檔案能配置的屬性參照
Java 反射機制詳解(四)
Java 反射機制詳解(四) 4. 反射與泛型 定義一個泛型類: public class DAO<T> { //根據id獲取一個物件 T get(Integer id){ return null; }
Redis底層詳解(四) 整數集合
一、集合概述 對於集合,STL 的 set 相信大家都不陌生,它的底層實現是紅黑樹。無論插入、刪除、查詢都是 O(log n) 的時間複雜度。當然,如果用雜湊表來實現集合,插入、刪除、查詢都可以達到 O(1)。那麼為什麼集合要用紅黑樹